Skip to content

feat: design system + analytics insights + interactive graph#15

Merged
kevintseng merged 28 commits intomainfrom
develop
Apr 19, 2026
Merged

feat: design system + analytics insights + interactive graph#15
kevintseng merged 28 commits intomainfrom
develop

Conversation

@kevintseng
Copy link
Copy Markdown
Contributor

Summary

  • Precision Engineer design system: Satoshi + Geist Mono fonts, cyan #00D6B4 accent, DESIGN.md as single source of truth
  • Analytics insights dashboard: Health Score (0-100), 30-day memory timeline sparkline, value metrics (recalls, lessons), cleanup suggestions with one-click archive
  • Interactive knowledge graph: Type filter checkboxes, search with highlight, ego graph mode (click to focus), recency heatmap, orphan detection
  • Feedback widget: Bug/Feature/Question selector, system info toggle, pre-filled GitHub issue
  • Type safety: Zero as any in dashboard/src/, proper TypeScript interfaces throughout
  • i18n: ~50 new keys across all 11 locales
  • Code review: Claude comprehensive (Dim 11+15) + Codex gpt-5.4, 10 issues found and fixed

Test plan

  • 408 tests passing (26 files), including 6 new analytics tests
  • npm run test:packaged smoke test passing
  • Dashboard visually verified via browse screenshots
  • /v1/analytics endpoint returns correct data shape
  • Graph tab renders with force-directed layout + cooling
  • All review findings (Claude + Codex) fixed and committed

🤖 Generated with Claude Code

kevintseng and others added 28 commits April 18, 2026 13:10
…ating

The overwrite merge strategy previously archived the entity then called
createEntity(), which has reactivation logic that un-archives and
APPENDS new observations to the old ones. Now uses clearEntityData()
to delete observations + tags (keeping the entity row) before
re-populating, ensuring overwrite truly replaces content.
exportMemories() previously applied namespace filter AFTER the SQL
LIMIT, so if limit=10 and the first 10 entities were all 'personal',
requesting namespace='team' returned 0 results even if team entities
existed. Now passes namespace directly to kg.search() which applies
the filter in the SQL WHERE clause before LIMIT.
Add embedder.ts with local ONNX embedding generation using
Xenova/all-MiniLM-L6-v2. Provides isEmbeddingAvailable(), embedText(),
embedAndStore(), and vectorSearch() — all gracefully degrade when the
package is missing. Fire-and-forget design keeps remember() synchronous.
- remember(): fire-and-forget embedAndStore() after entity creation
  (non-blocking, keeps function synchronous)
- recallEnhanced(): vector search supplements FTS5 results in both
  LLM expansion path and Level 0 fallback path
- Graceful degradation: if embeddings unavailable, search falls back
  to FTS5-only (no behavioral change for existing users)
Graph tab: force-directed canvas visualization of entity relationships
with drag, hover tooltips, and type-based coloring. No external deps.

Lessons tab: structured lesson_learned cards with parsed Error/Root
cause/Fix/Prevention fields, severity coloring, and confidence badges.

Both tabs include translations for all 11 supported languages.
…ph + Lessons

- Neural embeddings: @xenova/transformers (all-MiniLM-L6-v2, 384-dim)
  integrated into remember (fire-and-forget) and recallEnhanced (hybrid search)
- Fix overwrite import: clearEntityData() replaces archive+reactivate
- Fix namespace export: filter at query level, not post-filter
- Dashboard: 7 tabs (+ Graph with force-directed canvas, + Lessons with cards)
- 402 tests across 25 files
Replace generic Inter/zinc/blue aesthetic with distinctive design:
- Satoshi font (Fontshare) + Geist Mono (Google Fonts)
- Cyan accent #00D6B4 replacing overused blue-500
- Compact 4px spacing, 8px radius, zero decoration
- All hardcoded colors in LessonsTab/GraphTab replaced with
  design system tokens
- DESIGN.md added as single source of truth for all visual decisions

Co-Authored-By: Claude <noreply@anthropic.com>
…polish

- FeedbackWidget component: type selection (Bug/Feature/Question),
  description textarea, system info toggle, opens pre-filled GitHub issue
- Full i18n: 7 new keys across all 11 locales
- Restore interaction feedback: header backdrop-filter, card/stat hover
  shadows, input focus ring, feedback button lift effect, connection dot glow

Co-Authored-By: Claude <noreply@anthropic.com>
- FINDING-001/002/003: nav tabs min-height 44px, pagination buttons
  enlarged, border-bottom widened to 2px for better active indicator
- FINDING-005: search tab shows hint text when no search performed
  (i18n: search.hint added to all 11 locales)

Co-Authored-By: Claude <noreply@anthropic.com>
…l 11 locales

AnalyticsTab now fetches from both /v1/stats and /v1/analytics in
parallel, rendering HealthScore, MemoryTimeline, ValueMetrics, and
CleanupSuggestions components. Added 24 new i18n keys across all 11
locales (en, zh-TW, zh-CN, ja, ko, pt, fr, de, vi, es, th).
- HealthFactor: number → {score, weight, detail} object
- Timeline: day → date field name
- ValueMetrics: lessonsSaved → lessonsWithWarnings

Co-Authored-By: Claude <noreply@anthropic.com>
…, recency heatmap, orphans

- Type filter checkboxes: toggle visibility per entity type with colored dots
- Search with highlight: 260px input, cyan glow ring on matches, auto-center single match
- Ego graph: click node to focus on 1-degree neighborhood, banner with Show All
- Recency heatmap: opacity based on last_accessed_at age (0.15-1.0 range)
- Orphan indicator: dashed border for zero-relation nodes, 3rd stat card
- Tooltip shows name, type, and age; edge labels hidden when >30 visible edges
- Node labels only on hover/focus/match to reduce clutter
- Performance: skip distant repulsion pairs for N>200 nodes
- i18n: added graph.orphans/search/matches/focusMode/showAll/clickHint to all 11 locales
useEffect with [data] dependency fired while loading=true, when the
canvas element wasn't in the DOM yet (loading spinner rendered instead).
Added loading to dependency array and early return guard.

Co-Authored-By: Claude <noreply@anthropic.com>
CRITICAL: /v1/analytics staleEntities query now returns id + days_unused
  (was returning last_accessed_at without id — frontend contract mismatch)
CRITICAL: Added IS NULL guard for last_accessed_at (test had it, prod didn't)
MAJOR: Replaced (window as any).__graphReheat with proper global type declaration

Found by comprehensive-code-review Dim 11 (Reality Check) + Dim 15 (Honesty Audit)

Co-Authored-By: Claude <noreply@anthropic.com>
- SettingsTab: setLocale(value as any) → as Locale (proper type)
- SettingsTab: llm: any → { provider, model?, apiKey? } (typed)
- api.ts: ConfigData capabilities.llm: any → LlmConfig interface
- api.ts: fetchLessons (result as any).entities → proper cast

Zero `as any` remaining in dashboard/src/

Co-Authored-By: Claude <noreply@anthropic.com>
P2-1: Use SQLite datetime() for timestamp comparison instead of
  JS toISOString() — avoids format mismatch on boundary dates
P2-2: Graph hit-testing now skips hidden nodes (type filter + ego mode)
  — prevents tooltip/click on invisible nodes
P2-3: Coverage percentages use full type distribution total as
  denominator, not just top-8 slice — fixes inflated percentages

Found by Codex gpt-5.4 code review

Co-Authored-By: Claude <noreply@anthropic.com>
Comment on lines +302 to +441
app.get('/v1/analytics', (_req, res) => {
try {
const db = getDatabase();

// Use SQLite datetime() for consistent comparison regardless of timestamp format
const thirtyDaysAgo = "datetime('now', '-30 days')";
const sevenDaysAgo = "datetime('now', '-7 days')";

// --- Health Score ---
const totalActive = (db.prepare(
"SELECT COUNT(*) as c FROM entities WHERE status = 'active'"
).get() as CountRow).c;

// Activity: % of active entities accessed in last 30 days
const recentlyAccessed = (db.prepare(
`SELECT COUNT(*) as c FROM entities WHERE status = 'active' AND last_accessed_at >= ${thirtyDaysAgo}`
).get() as CountRow).c;
const activityRatio = totalActive > 0 ? recentlyAccessed / totalActive : 0;

// Quality: % of active entities with confidence > 0.7
const highConfidence = (db.prepare(
"SELECT COUNT(*) as c FROM entities WHERE status = 'active' AND confidence > 0.7"
).get() as CountRow).c;
const qualityRatio = totalActive > 0 ? highConfidence / totalActive : 0;

// Freshness: new entities this week relative to total (capped at 1.0)
const newThisWeek = (db.prepare(
`SELECT COUNT(*) as c FROM entities WHERE created_at >= ${sevenDaysAgo}`
).get() as CountRow).c;
const freshnessRatio = totalActive > 0 ? Math.min(newThisWeek / totalActive, 1.0) : 0;

// Lessons: lesson_learned entity count, 5+ = full score
const lessonCount = (db.prepare(
"SELECT COUNT(*) as c FROM entities WHERE type = 'lesson_learned'"
).get() as CountRow).c;
const lessonRatio = Math.min(lessonCount / 5, 1.0);

const healthScore = Math.round(
activityRatio * 30 + qualityRatio * 30 + freshnessRatio * 20 + lessonRatio * 20
);

const healthFactors = {
activity: { score: Math.round(activityRatio * 30), weight: 30, detail: `${recentlyAccessed}/${totalActive} active entities accessed in last 30 days` },
quality: { score: Math.round(qualityRatio * 30), weight: 30, detail: `${highConfidence}/${totalActive} active entities with confidence > 0.7` },
freshness: { score: Math.round(freshnessRatio * 20), weight: 20, detail: `${newThisWeek} new entities this week` },
lessons: { score: Math.round(lessonRatio * 20), weight: 20, detail: `${lessonCount} lessons learned` },
};

// --- Timeline (last 30 days) ---
const createdTimeline = db.prepare(`
SELECT DATE(created_at) as day, COUNT(*) as created
FROM entities
WHERE created_at >= ${thirtyDaysAgo}
GROUP BY DATE(created_at)
ORDER BY day
`).all() as Array<{ day: string; created: number }>;

const recalledTimeline = db.prepare(`
SELECT DATE(last_accessed_at) as day, COUNT(*) as recalled
FROM entities
WHERE last_accessed_at >= ${thirtyDaysAgo}
GROUP BY DATE(last_accessed_at)
ORDER BY day
`).all() as Array<{ day: string; recalled: number }>;

// Merge into daily buckets
const timelineMap = new Map<string, { date: string; created: number; recalled: number }>();
for (const row of createdTimeline) {
timelineMap.set(row.day, { date: row.day, created: row.created, recalled: 0 });
}
for (const row of recalledTimeline) {
const existing = timelineMap.get(row.day);
if (existing) {
existing.recalled = row.recalled;
} else {
timelineMap.set(row.day, { date: row.day, created: 0, recalled: row.recalled });
}
}
const timeline = Array.from(timelineMap.values()).sort((a, b) => a.date.localeCompare(b.date));

// --- Value Metrics ---
const totalRecalls = (db.prepare(
"SELECT COALESCE(SUM(access_count), 0) as c FROM entities"
).get() as CountRow).c;

const lessonsWithWarnings = (db.prepare(
"SELECT COUNT(*) as c FROM entities WHERE type = 'lesson_learned' AND access_count > 0"
).get() as CountRow).c;

const typeDistribution = db.prepare(
"SELECT type, COUNT(*) as count FROM entities GROUP BY type ORDER BY count DESC"
).all();

const valueMetrics = {
totalRecalls,
lessonCount,
lessonsWithWarnings,
typeDistribution,
};

// --- Cleanup Suggestions ---
const staleEntities = db.prepare(`
SELECT id, name, type, confidence,
CAST((julianday('now') - julianday(COALESCE(last_accessed_at, created_at))) AS INTEGER) as days_unused
FROM entities
WHERE status = 'active'
AND confidence < 0.4
AND (last_accessed_at IS NULL OR last_accessed_at < ${thirtyDaysAgo})
ORDER BY confidence ASC
LIMIT 10
`).all();

const duplicateCandidates = db.prepare(`
SELECT e1.name as name1, e2.name as name2, e1.type
FROM entities e1
JOIN entities e2 ON e1.id < e2.id AND e1.type = e2.type
WHERE e1.status = 'active' AND e2.status = 'active'
AND (INSTR(LOWER(e1.name), LOWER(e2.name)) > 0 OR INSTR(LOWER(e2.name), LOWER(e1.name)) > 0)
LIMIT 5
`).all();

const cleanup = {
staleEntities,
duplicateCandidates,
};

res.json({
success: true,
data: {
healthScore,
healthFactors,
timeline,
valueMetrics,
cleanup,
},
});
} catch (err: any) {
res.status(500).json({ success: false, error: err.message });
}
});
import os from 'os';
import path from 'path';
import Database from 'better-sqlite3';
import { openDatabase, closeDatabase, getDatabase } from '../../src/db.js';
@kevintseng kevintseng merged commit b6b491f into main Apr 19, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants