mirror of
https://github.com/samvallad33/vestige.git
synced 2026-05-03 21:02:37 +02:00
fix: comprehensive audit fixes for dashboard and backend
Backend: - Emit WebSocket events from REST delete/promote/demote handlers - Emit DreamStarted/ConsolidationStarted from MCP tool dispatch - Add path validation in backup_to() for defense-in-depth Dashboard: - Fix ConnectionDiscovered field names (source_id/target_id) - Fix $effect → onMount in settings (prevents infinite loop) - Fix $derived → $derived.by in RetentionCurve - Fix field name mismatches in settings (nodesProcessed, etc.) - Fix nested <button> → <span role="button"> in memories - Fix unhandled Promise rejection in stats consolidation - Add missing EVENT_TYPE_COLORS entries - Add Three.js resource disposal and event listener cleanup - Eliminate duplicate root page, redirect to /graph - Update nav links and keyboard shortcuts to /graph All 734+ tests passing, 22MB binary, zero build warnings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
22831af509
commit
ec2af6e71b
220 changed files with 347 additions and 443 deletions
|
|
@ -69,6 +69,19 @@
|
|||
onDestroy(() => {
|
||||
cancelAnimationFrame(animationId);
|
||||
window.removeEventListener('resize', onResize);
|
||||
container?.removeEventListener('pointermove', onPointerMove);
|
||||
container?.removeEventListener('click', onClick);
|
||||
// Dispose Three.js resources to prevent GPU memory leaks
|
||||
scene?.traverse((obj: THREE.Object3D) => {
|
||||
if (obj instanceof THREE.Mesh || obj instanceof THREE.InstancedMesh) {
|
||||
obj.geometry?.dispose();
|
||||
if (Array.isArray(obj.material)) {
|
||||
obj.material.forEach((m: THREE.Material) => m.dispose());
|
||||
} else if (obj.material) {
|
||||
(obj.material as THREE.Material).dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
renderer?.dispose();
|
||||
composer?.dispose();
|
||||
});
|
||||
|
|
@ -554,9 +567,9 @@
|
|||
break;
|
||||
}
|
||||
case 'ConnectionDiscovered': {
|
||||
const data = event.data as { source?: string; target?: string };
|
||||
const srcPos = data.source ? nodePositions.get(data.source) : null;
|
||||
const tgtPos = data.target ? nodePositions.get(data.target) : null;
|
||||
const data = event.data as { source_id?: string; target_id?: string };
|
||||
const srcPos = data.source_id ? nodePositions.get(data.source_id) : null;
|
||||
const tgtPos = data.target_id ? nodePositions.get(data.target_id) : null;
|
||||
if (srcPos && tgtPos) {
|
||||
createConnectionFlash(srcPos, tgtPos, new THREE.Color(0xf59e0b));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
}
|
||||
|
||||
// Generate SVG path for the decay curve
|
||||
let curvePath = $derived(() => {
|
||||
let curvePath = $derived.by(() => {
|
||||
const points: string[] = [];
|
||||
const maxDays = Math.max(stability * 3, 30);
|
||||
const padding = 4;
|
||||
|
|
@ -56,10 +56,10 @@
|
|||
<line x1="4" y1="{4 + (height - 8) * 0.8}" x2="{width - 4}" y2="{4 + (height - 8) * 0.8}" stroke="#ef444430" stroke-width="0.5" stroke-dasharray="2,4" />
|
||||
|
||||
<!-- Decay curve -->
|
||||
<path d={curvePath()} fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round" />
|
||||
<path d={curvePath} fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round" />
|
||||
|
||||
<!-- Fill under curve -->
|
||||
<path d="{curvePath()} L{width - 4},{height - 4} L4,{height - 4} Z" fill="url(#curveGrad)" opacity="0.15" />
|
||||
<path d="{curvePath} L{width - 4},{height - 4} L4,{height - 4} Z" fill="url(#curveGrad)" opacity="0.15" />
|
||||
|
||||
<!-- Current retention dot -->
|
||||
<circle cx="4" cy="{4 + (1 - retention) * (height - 8)}" r="3" fill={retColor(retention)} />
|
||||
|
|
|
|||
|
|
@ -196,12 +196,17 @@ export const EVENT_TYPE_COLORS: Record<string, string> = {
|
|||
MemoryCreated: '#10b981',
|
||||
MemoryUpdated: '#3b82f6',
|
||||
MemoryDeleted: '#ef4444',
|
||||
MemoryPromoted: '#22c55e',
|
||||
MemoryDemoted: '#f97316',
|
||||
SearchPerformed: '#6366f1',
|
||||
DreamStarted: '#8b5cf6',
|
||||
DreamProgress: '#7c3aed',
|
||||
DreamCompleted: '#a855f7',
|
||||
ConsolidationStarted: '#f59e0b',
|
||||
ConsolidationCompleted: '#f97316',
|
||||
RetentionDecayed: '#ef4444',
|
||||
ConnectionDiscovered: '#06b6d4',
|
||||
ActivationSpread: '#14b8a6',
|
||||
ImportanceScored: '#ec4899',
|
||||
Heartbeat: '#6b7280',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -124,12 +124,15 @@
|
|||
<div>Created: {new Date(memory.createdAt).toLocaleDateString()}</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick={(e) => { e.stopPropagation(); api.memories.promote(memory.id); }}
|
||||
class="px-3 py-1.5 bg-recall/20 text-recall text-xs rounded hover:bg-recall/30">Promote</button>
|
||||
<button onclick={(e) => { e.stopPropagation(); api.memories.demote(memory.id); }}
|
||||
class="px-3 py-1.5 bg-decay/20 text-decay text-xs rounded hover:bg-decay/30">Demote</button>
|
||||
<button onclick={(e) => { e.stopPropagation(); api.memories.delete(memory.id); loadMemories(); }}
|
||||
class="px-3 py-1.5 bg-decay/10 text-decay/60 text-xs rounded hover:bg-decay/20 ml-auto">Delete</button>
|
||||
<span role="button" tabindex="0" onclick={(e) => { e.stopPropagation(); api.memories.promote(memory.id); }}
|
||||
onkeydown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); api.memories.promote(memory.id); } }}
|
||||
class="px-3 py-1.5 bg-recall/20 text-recall text-xs rounded hover:bg-recall/30 cursor-pointer select-none">Promote</span>
|
||||
<span role="button" tabindex="0" onclick={(e) => { e.stopPropagation(); api.memories.demote(memory.id); }}
|
||||
onkeydown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); api.memories.demote(memory.id); } }}
|
||||
class="px-3 py-1.5 bg-decay/20 text-decay text-xs rounded hover:bg-decay/30 cursor-pointer select-none">Demote</span>
|
||||
<span role="button" tabindex="0" onclick={async (e) => { e.stopPropagation(); await api.memories.delete(memory.id); loadMemories(); }}
|
||||
onkeydown={async (e) => { if (e.key === 'Enter') { e.stopPropagation(); await api.memories.delete(memory.id); loadMemories(); } }}
|
||||
class="px-3 py-1.5 bg-decay/10 text-decay/60 text-xs rounded hover:bg-decay/20 ml-auto cursor-pointer select-none">Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$stores/api';
|
||||
import { isConnected, memoryCount, avgRetention } from '$stores/websocket';
|
||||
|
||||
|
|
@ -16,7 +17,7 @@
|
|||
// Health
|
||||
let health = $state<Record<string, unknown> | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
onMount(() => {
|
||||
loadAllData();
|
||||
});
|
||||
|
||||
|
|
@ -112,21 +113,21 @@
|
|||
{#if consolidationResult}
|
||||
<div class="bg-deep/50 p-3 rounded-lg border border-subtle/10">
|
||||
<div class="grid grid-cols-3 gap-3 text-center">
|
||||
{#if consolidationResult.processed !== undefined}
|
||||
{#if consolidationResult.nodesProcessed !== undefined}
|
||||
<div>
|
||||
<div class="text-lg text-text font-semibold">{consolidationResult.processed}</div>
|
||||
<div class="text-lg text-text font-semibold">{consolidationResult.nodesProcessed}</div>
|
||||
<div class="text-[10px] text-muted">Processed</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if consolidationResult.decayed !== undefined}
|
||||
{#if consolidationResult.decayApplied !== undefined}
|
||||
<div>
|
||||
<div class="text-lg text-decay font-semibold">{consolidationResult.decayed}</div>
|
||||
<div class="text-lg text-decay font-semibold">{consolidationResult.decayApplied}</div>
|
||||
<div class="text-[10px] text-muted">Decayed</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if consolidationResult.embedded !== undefined}
|
||||
{#if consolidationResult.embeddingsGenerated !== undefined}
|
||||
<div>
|
||||
<div class="text-lg text-synapse-glow font-semibold">{consolidationResult.embedded}</div>
|
||||
<div class="text-lg text-synapse-glow font-semibold">{consolidationResult.embeddingsGenerated}</div>
|
||||
<div class="text-[10px] text-muted">Embedded</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -181,10 +182,10 @@
|
|||
<span class="text-recall">◫</span> Retention Distribution
|
||||
</h2>
|
||||
<div class="p-4 bg-surface/30 border border-subtle/20 rounded-lg">
|
||||
{#if retentionDist.buckets && Array.isArray(retentionDist.buckets)}
|
||||
{#if retentionDist.distribution && Array.isArray(retentionDist.distribution)}
|
||||
<div class="flex items-end gap-1 h-32">
|
||||
{#each retentionDist.buckets as bucket, i}
|
||||
{@const maxCount = Math.max(...(retentionDist.buckets as {count: number}[]).map((b: {count: number}) => b.count), 1)}
|
||||
{#each retentionDist.distribution as bucket, i}
|
||||
{@const maxCount = Math.max(...(retentionDist.distribution as {count: number}[]).map((b: {count: number}) => b.count), 1)}
|
||||
{@const height = ((bucket as {count: number}).count / maxCount) * 100}
|
||||
{@const color = i < 2 ? '#ef4444' : i < 4 ? '#f59e0b' : i < 7 ? '#6366f1' : '#10b981'}
|
||||
<div class="flex-1 flex flex-col items-center gap-1">
|
||||
|
|
|
|||
|
|
@ -27,9 +27,12 @@
|
|||
}
|
||||
|
||||
async function runConsolidation() {
|
||||
try { await api.consolidate(); } catch { /* ignore */ }
|
||||
// Refresh
|
||||
[stats, health, retention] = await Promise.all([api.stats(), api.health(), api.retentionDistribution()]);
|
||||
try {
|
||||
await api.consolidate();
|
||||
[stats, health, retention] = await Promise.all([api.stats(), api.health(), api.retentionDistribution()]);
|
||||
} catch {
|
||||
// API not available
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
}
|
||||
// Single-key navigation shortcuts
|
||||
const shortcutMap: Record<string, string> = {
|
||||
g: '/', m: '/memories', t: '/timeline', f: '/feed',
|
||||
g: '/graph', m: '/memories', t: '/timeline', f: '/feed',
|
||||
e: '/explore', i: '/intentions', s: '/stats',
|
||||
};
|
||||
const target = shortcutMap[e.key.toLowerCase()];
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
});
|
||||
|
||||
const nav = [
|
||||
{ href: '/', label: 'Graph', icon: '◎', shortcut: 'G' },
|
||||
{ href: '/graph', label: 'Graph', icon: '◎', shortcut: 'G' },
|
||||
{ href: '/memories', label: 'Memories', icon: '◈', shortcut: 'M' },
|
||||
{ href: '/timeline', label: 'Timeline', icon: '◷', shortcut: 'T' },
|
||||
{ href: '/feed', label: 'Feed', icon: '◉', shortcut: 'F' },
|
||||
|
|
@ -69,9 +69,8 @@
|
|||
const mobileNav = nav.slice(0, 5);
|
||||
|
||||
function isActive(href: string, currentPath: string): boolean {
|
||||
// Strip base prefix for comparison
|
||||
const path = currentPath.startsWith(base) ? currentPath.slice(base.length) || '/' : currentPath;
|
||||
if (href === '/') return path === '/' || path === '/graph';
|
||||
if (href === '/graph') return path === '/' || path === '/graph';
|
||||
return path.startsWith(href);
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +93,7 @@
|
|||
<!-- Desktop Sidebar (hidden on mobile) -->
|
||||
<nav class="hidden md:flex w-16 lg:w-56 flex-shrink-0 bg-abyss border-r border-subtle/30 flex-col">
|
||||
<!-- Logo -->
|
||||
<a href="/" class="flex items-center gap-3 px-4 py-5 border-b border-subtle/20">
|
||||
<a href="/graph" class="flex items-center gap-3 px-4 py-5 border-b border-subtle/20">
|
||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-dream to-synapse flex items-center justify-center text-bright text-sm font-bold">
|
||||
V
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,158 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import Graph3D from '$components/Graph3D.svelte';
|
||||
import { api } from '$stores/api';
|
||||
import { eventFeed } from '$stores/websocket';
|
||||
import type { GraphResponse, Memory } from '$types';
|
||||
|
||||
let graphData: GraphResponse | null = $state(null);
|
||||
let selectedMemory: Memory | null = $state(null);
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
let isDreaming = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
graphData = await api.graph({ max_nodes: 150, depth: 3 });
|
||||
} catch (e) {
|
||||
error = 'No memories yet. Start using Vestige to see your memory graph.';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function triggerDream() {
|
||||
isDreaming = true;
|
||||
try {
|
||||
const result = await api.dream();
|
||||
// Reload graph with new connections
|
||||
graphData = await api.graph({ max_nodes: 150, depth: 3 });
|
||||
} catch {
|
||||
// Dream failed silently
|
||||
} finally {
|
||||
isDreaming = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onNodeSelect(nodeId: string) {
|
||||
try {
|
||||
selectedMemory = await api.memories.get(nodeId);
|
||||
} catch {
|
||||
selectedMemory = null;
|
||||
}
|
||||
}
|
||||
onMount(() => goto('/graph', { replaceState: true }));
|
||||
</script>
|
||||
|
||||
<div class="h-full relative">
|
||||
<!-- 3D Graph fills the viewport -->
|
||||
{#if loading}
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div class="text-center space-y-4">
|
||||
<div class="w-16 h-16 mx-auto rounded-full border-2 border-synapse/30 border-t-synapse animate-spin"></div>
|
||||
<p class="text-dim text-sm">Loading memory graph...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div class="text-center space-y-4 max-w-md px-8">
|
||||
<div class="text-4xl">◎</div>
|
||||
<h2 class="text-xl text-bright">Your Mind Awaits</h2>
|
||||
<p class="text-dim text-sm">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if graphData}
|
||||
<Graph3D
|
||||
nodes={graphData.nodes}
|
||||
edges={graphData.edges}
|
||||
centerId={graphData.center_id}
|
||||
events={$eventFeed}
|
||||
{isDreaming}
|
||||
onSelect={onNodeSelect}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Floating controls -->
|
||||
<div class="absolute top-4 left-4 flex gap-2 z-10">
|
||||
<button
|
||||
onclick={triggerDream}
|
||||
disabled={isDreaming}
|
||||
class="px-4 py-2 rounded-lg bg-dream/20 border border-dream/40 text-dream-glow text-sm
|
||||
hover:bg-dream/30 transition-all disabled:opacity-50 backdrop-blur-sm
|
||||
{isDreaming ? 'glow-dream animate-pulse-glow' : ''}"
|
||||
>
|
||||
{isDreaming ? '◎ Dreaming...' : '◎ Dream'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Floating stats -->
|
||||
<div class="absolute top-4 right-4 z-10 text-xs text-dim backdrop-blur-sm bg-abyss/60 rounded-lg px-3 py-2 border border-subtle/20">
|
||||
{#if graphData}
|
||||
<div>{graphData.nodeCount} nodes / {graphData.edgeCount} edges</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Selected memory detail panel -->
|
||||
{#if selectedMemory}
|
||||
<div class="absolute right-0 top-0 h-full w-96 bg-abyss/95 backdrop-blur-xl border-l border-subtle/30 p-6 overflow-y-auto z-20">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<h3 class="text-bright text-sm font-semibold">Memory Detail</h3>
|
||||
<button onclick={() => selectedMemory = null} class="text-dim hover:text-text text-lg">×</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Type badge -->
|
||||
<div class="flex gap-2">
|
||||
<span class="px-2 py-0.5 rounded text-xs bg-synapse/20 text-synapse-glow">{selectedMemory.nodeType}</span>
|
||||
{#each selectedMemory.tags as tag}
|
||||
<span class="px-2 py-0.5 rounded text-xs bg-surface text-dim">{tag}</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="text-sm text-text leading-relaxed whitespace-pre-wrap">{selectedMemory.content}</div>
|
||||
|
||||
<!-- Retention bar -->
|
||||
<div>
|
||||
<div class="flex justify-between text-xs text-dim mb-1">
|
||||
<span>Retention</span>
|
||||
<span>{(selectedMemory.retentionStrength * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="h-2 bg-surface rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
style="width: {selectedMemory.retentionStrength * 100}%; background: {
|
||||
selectedMemory.retentionStrength > 0.7 ? '#10b981' :
|
||||
selectedMemory.retentionStrength > 0.4 ? '#f59e0b' : '#ef4444'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="text-xs text-dim space-y-1">
|
||||
<div>Created: {new Date(selectedMemory.createdAt).toLocaleDateString()}</div>
|
||||
<div>Reviews: {selectedMemory.reviewCount ?? 0}</div>
|
||||
{#if selectedMemory.source}
|
||||
<div>Source: {selectedMemory.source}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onclick={() => selectedMemory && api.memories.promote(selectedMemory.id)}
|
||||
class="flex-1 px-3 py-2 rounded bg-recall/20 text-recall text-xs hover:bg-recall/30 transition"
|
||||
>
|
||||
Promote
|
||||
</button>
|
||||
<button
|
||||
onclick={() => selectedMemory && api.memories.demote(selectedMemory.id)}
|
||||
class="flex-1 px-3 py-2 rounded bg-decay/20 text-decay text-xs hover:bg-decay/30 transition"
|
||||
>
|
||||
Demote
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue