Add query bar, maintenance panel, and heartbeat system

- Query bar with organized/grouped search results in portal
- Maintenance panel UI for triggering and viewing maintenance status
- Heartbeat service with periodic maintenance and dirty-tracking
- Query organizer for grouping search results by tag/kind/parent
- Slide-up animation for query panel
This commit is contained in:
2026-02-03 00:55:08 +01:00
parent f65653e260
commit af568f81c2
11 changed files with 785 additions and 2 deletions

View File

@@ -4,8 +4,10 @@ import GraphView from './components/GraphView';
import Sidebar from './components/Sidebar';
import NodePanel from './components/NodePanel';
import AddNodeModal from './components/AddNodeModal';
import QueryBar from './components/QueryBar';
import LinkModal from './components/LinkModal';
import Toast from './components/Toast';
import MaintenancePanel from './components/MaintenancePanel';
export default function App() {
const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -13,6 +15,8 @@ export default function App() {
const [linkFromId, setLinkFromId] = useState<string | null>(null);
const [toast, setToast] = useState<string | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [showQuery, setShowQuery] = useState(false);
const [showMaintenance, setShowMaintenance] = useState(false);
const qc = useQueryClient();
const refresh = useCallback(() => {
@@ -34,12 +38,13 @@ export default function App() {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (selectedId) setSelectedId(null);
else if (showQuery) setShowQuery(false);
else if (drawerOpen) setDrawerOpen(false);
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [selectedId, drawerOpen]);
}, [selectedId, drawerOpen, showQuery]);
return (
<div className="h-screen w-screen overflow-hidden relative">
@@ -64,6 +69,20 @@ export default function App() {
>
+
</button>
<button
onClick={() => setShowQuery(!showQuery)}
className="w-12 h-12 rounded-full bg-purple-600/90 backdrop-blur border border-purple-500 text-white hover:bg-purple-500 shadow-lg flex items-center justify-center text-lg font-bold"
title="Query memory"
>
?
</button>
<button
onClick={() => setShowMaintenance(!showMaintenance)}
className="w-12 h-12 rounded-full bg-emerald-600/90 backdrop-blur border border-emerald-500 text-white hover:bg-emerald-500 shadow-lg flex items-center justify-center text-lg"
title="Maintenance"
>
&#x2699;
</button>
</div>
{/* Sidebar drawer — slides in from left */}
@@ -106,6 +125,22 @@ export default function App() {
</div>
)}
{/* Query panel — slides up from bottom */}
{showQuery && (
<div className="fixed inset-0 z-40" onClick={() => setShowQuery(false)}>
<div className="absolute inset-0 bg-black/40" />
<div
className="absolute bottom-0 left-0 right-0 h-[70vh] animate-slide-in-up"
onClick={(e) => e.stopPropagation()}
>
<QueryBar
onSelectNode={(id) => { setShowQuery(false); selectNode(id); }}
onClose={() => setShowQuery(false)}
/>
</div>
</div>
)}
{showAddNode && (
<AddNodeModal
onClose={() => setShowAddNode(false)}
@@ -119,6 +154,11 @@ export default function App() {
onCreated={() => { refresh(); notify('Edge created'); }}
/>
)}
{showMaintenance && (
<div className="fixed bottom-20 left-5 z-50">
<MaintenancePanel onClose={() => setShowMaintenance(false)} onNotify={(msg) => { notify(msg); refresh(); }} />
</div>
)}
{toast && <Toast message={toast} />}
</div>
);