feat(web): MazeNET 7a — canvas chrome + node-head visuals

Toolbar (RESET VIEW / AUTO-LAYOUT), status bar (GRAPH LIVE + pan + as-of
timestamp), 4-row legend, and archetype icon + status dot in each node
head.
This commit is contained in:
2026-04-22 15:54:11 -04:00
parent 91111ea7ee
commit 6fbac5d057
4 changed files with 95 additions and 7 deletions

View File

@@ -20,6 +20,7 @@ import { useMazeInteraction, type PaletteDrag } from './useMazeInteraction';
import { useLayoutPersistor } from './useMazeLayoutStore';
import { useTopologyStream, type TopologyStreamEvent } from './useTopologyStream';
import { ARCHETYPES as DEFAULT_ARCHETYPES } from './data';
import { useToast } from '../Toasts/useToast';
/* Short unique suffix for default names — avoids the DB uniqueness
* constraint regardless of delete/re-add sequencing on the client. */
@@ -33,6 +34,7 @@ const hex4 = (): string => {
const MazeNET: React.FC = () => {
const api = useMazeApi();
const navigate = useNavigate();
const { push: pushToast } = useToast();
const [params] = useSearchParams();
const topologyId = params.get('topology') ?? '';
@@ -457,6 +459,7 @@ const MazeNET: React.FC = () => {
* keepalives. On any state-transition event we refetch; DB is the
* source of truth and the bus is at-most-once. */
const [streamLive, setStreamLive] = useState(false);
const [lastEventAt, setLastEventAt] = useState<Date | null>(null);
const streamEnabled = topoStatus === 'active' || topoStatus === 'degraded';
const onStreamEvent = useCallback((event: TopologyStreamEvent) => {
// Flip LIVE only on named, purposeful events — not incidental keepalives.
@@ -464,6 +467,7 @@ const MazeNET: React.FC = () => {
|| event.name.startsWith('mutation.')
|| event.name === 'status') {
setStreamLive(true);
setLastEventAt(new Date());
}
if (event.name === 'mutation.failed') {
const p = event.payload ?? {};
@@ -529,7 +533,7 @@ const MazeNET: React.FC = () => {
</div>
</div>
<div className="maze-page-actions">
<button type="button" className="maze-btn ghost" onClick={() => navigate('/topologies')}>
<button type="button" className="maze-btn ghost" onClick={() => navigate('/mazenet')}>
<ArrowLeft size={12} /> TOPOLOGIES
</button>
<button type="button" className="maze-btn ghost" onClick={() => setInspectorOpen((o) => !o)}>
@@ -576,6 +580,10 @@ const MazeNET: React.FC = () => {
onNetContextMenu={onNetContextMenu}
onEdgeContextMenu={onEdgeContextMenu}
onCanvasContextMenu={onCanvasContextMenu}
onResetView={interaction.resetPan}
onAutoLayout={() => pushToast({ text: 'AUTO-LAYOUT COMING SOON', tone: 'violet', icon: 'info' })}
sseConnected={streamLive}
lastEventAt={lastEventAt}
/>
{ctxMenu && (
<ContextMenu x={ctxMenu.x} y={ctxMenu.y} items={ctxMenu.items} onClose={() => setCtxMenu(null)} />