Memory Dashboard

Visual web UI for exploring Claude's vector memory system with semantic search, knowledge graphs, and real-time analytics

1. Problem Statement

The Challenge

Claude's memory system stores episodic memories, learnings, trajectories, procedures, and benchmarks across multiple Qdrant collections and Supabase tables. While the MCP tools (memory_recall, memory_store, procedure, etc.) provide programmatic access, there's no visual interface for:

The memory system is mission-critical for agent context continuity, but without visibility into what's stored, how it's connected, and when it was learned, operators are flying blind.

Current Pain Points

Solution: Memory Dashboard

A localhost web UI that provides visual access to the entire memory system. Built with FastAPI (Python 3.12), HTMX for reactive updates, Chart.js for analytics, and D3.js for knowledge graph visualization. Docker-deployed for portability and isolation.

Key capabilities:

2. Architecture Overview

Memory Dashboard Architecture

System Components

The dashboard is a three-tier web application with clean separation of concerns:

Layer Technology Responsibility
Frontend HTMX + Chart.js + D3.js Reactive UI, charts, knowledge graph, pagination
Backend FastAPI (Python 3.12) API routes, Qdrant queries, Supabase access, embedding generation
Data Qdrant + Supabase Vector memories (8 collections), relational contacts/decisions

Data Flow:

  1. User interacts with HTMX UI (filter change, search query, graph node click)
  2. HTMX sends AJAX request to FastAPI endpoint (/api/memories, /api/search, etc.)
  3. FastAPI queries Qdrant vector DB or Supabase relational DB
  4. For semantic search: query → Ollama embedding → Qdrant similarity search
  5. API returns JSON (paginated results, stats, graph nodes/edges)
  6. HTMX swaps HTML fragments OR Chart.js/D3.js re-renders visualizations

Qdrant Collections Schema

The memory system uses 8 Qdrant collections, each with 768-dimensional embeddings (Ollama nomic-embed-text):

claude_memories # Generic memories (type: learning, decision, contact, etc.) learnings # Extracted learnings from multi-step successes trajectories # Action sequences with outcomes procedures # Reusable step-by-step processes episodes # Conversation contexts and summaries memory_links # Relationships between memories (source_id → target_id) audit_log # Governance decisions and approval gates benchmarks # Performance baselines and test results

Common Metadata Fields:

Security Model

Localhost-Only Deployment
The dashboard binds to 127.0.0.1:8092 and is NOT exposed to external networks. No authentication layer needed since access requires local shell access.

Defense-in-Depth Protections:

3. Key Components

Dashboard View (/)

Landing page with high-level overview cards and charts:

Refresh Behavior: Dashboard auto-refreshes every 30 seconds via HTMX polling to reflect new memories added by active agent sessions.

Memory Browser (/memories)

Paginated table view with advanced filtering:

Performance Optimization: Qdrant scroll API used for pagination instead of offset-based queries. Scroll context cached in Redis for 5 minutes to enable fast forward/backward navigation.

Semantic Search (/search)

Natural language search across all collections using vector similarity:

  1. Query Input: User enters natural language query (e.g., "Docker networking issues on Ubuntu")
  2. Embedding Generation: FastAPI calls Ollama API (http://localhost:11434/api/embeddings) with model nomic-embed-text
  3. Vector Search: Qdrant searches all 8 collections for nearest neighbors (cosine similarity, top 50 results)
  4. Result Ranking: Results sorted by similarity score (0.0-1.0), then by recall_count as tiebreaker
  5. Display: Cards showing content, similarity score, type, project, tags, and "Why matched" snippet highlighting

Search Settings:

# Example API call POST /api/search { "query": "git workflow automation procedures", "collections": ["procedures", "learnings", "trajectories"], "score_threshold": 0.70, "max_results": 20, "max_sensitivity": "internal" }

Knowledge Graph (/graph)

D3.js force-directed graph visualization of memory relationships:

Performance Note: Graph rendering limited to 500 nodes. If query returns more, show "Top 500 by recall_count" warning and provide filter suggestions.

Contacts View (/contacts)

Specialized view for contact memories stored in Supabase contacts table:

Privacy Protection: Email addresses displayed with first 3 chars + ***@domain masking. Click "Reveal" button to show full email (logged to audit_log).

Decisions View (/decisions)

Audit log viewer for governance decisions from audit_log collection:

Analytics (/analytics)

Deep-dive charts and statistics:

4. Requirements

Backend Requirements

REQ-DASH-001
FastAPI application MUST run on Python 3.12+ with async/await request handlers for all API endpoints.
REQ-DASH-002
Application MUST bind to 127.0.0.1:8092 only (no external network exposure).
REQ-DASH-003
Qdrant connection MUST use environment variable QDRANT_URL (default: http://localhost:6333) with connection pooling.
REQ-DASH-004
Supabase connection MUST use environment variables SUPABASE_URL and SUPABASE_KEY with SDK initialization at app startup.
REQ-DASH-005
Ollama embeddings MUST use nomic-embed-text model at OLLAMA_URL (default: http://localhost:11434) with 10-second timeout.
REQ-DASH-006
All Qdrant queries MUST include error handling for collection not found, invalid scroll context, and timeout errors.
REQ-DASH-007
Pagination MUST use Qdrant scroll API with Redis-cached scroll contexts (5-minute TTL).
REQ-DASH-008
Semantic search MUST query all specified collections in parallel using asyncio.gather().
REQ-DASH-009
Knowledge graph data MUST include node metadata (type, project, recall_count) and edge metadata (link_strength, relation_type).
REQ-DASH-010
API responses MUST include proper HTTP status codes (200 OK, 400 Bad Request, 404 Not Found, 500 Internal Server Error) with error details in JSON.

Frontend Requirements

REQ-DASH-011
UI MUST use HTMX for reactive updates without full page reloads (hx-get, hx-post, hx-swap, hx-trigger).
REQ-DASH-012
Charts MUST use Chart.js v4+ with responsive configuration and dark theme color palette.
REQ-DASH-013
Knowledge graph MUST use D3.js v7+ force simulation with configurable charge strength, link distance, and collision radius.
REQ-DASH-014
All CDN-loaded scripts MUST include SRI hashes for Chart.js, D3.js, and HTMX.
REQ-DASH-015
Dashboard MUST auto-refresh every 30 seconds using hx-trigger="every 30s" on stats container.
REQ-DASH-016
Memory browser MUST support infinite scroll with HTMX intersection observer on sentinel div.
REQ-DASH-017
Search results MUST highlight matching text snippets using <mark> tags with yellow background.
REQ-DASH-018
Contact email masking MUST use pattern abc***@domain.com with "Reveal" button that logs to audit_log.
REQ-DASH-019
Graph node colors MUST follow type convention: learnings=blue (#60a5fa), trajectories=green (#2dd4bf), procedures=purple (#a78bfa), episodes=orange (#fb923c).
REQ-DASH-020
All forms MUST include client-side validation with instant feedback on invalid inputs.

API Endpoint Requirements

REQ-DASH-021
GET /health MUST return JSON with status: ok, Qdrant connection status, Supabase connection status, and Ollama availability.
REQ-DASH-022
GET /api/stats MUST return collection counts, type distribution, top 10 projects, last 90 days timeline, and sensitivity breakdown.
REQ-DASH-023
GET /api/memories MUST accept query params: type, project, tags, after, before, sensitivity, page, limit.
REQ-DASH-024
POST /api/search MUST accept JSON body with query, collections, score_threshold, max_results, max_sensitivity.
REQ-DASH-025
GET /api/graph-data MUST accept query params: types, project, min_link_strength, max_nodes.
REQ-DASH-026
GET /api/contacts MUST query Supabase contacts table with filters and return masked email addresses.
REQ-DASH-027
GET /api/decisions MUST query audit_log collection with filters: event_type, after, before, operator, outcome.
REQ-DASH-028
POST /api/contacts/:id/reveal-email MUST return full email and log action to audit_log with event_type=email_reveal.
REQ-DASH-029
GET /api/export/decisions MUST return CSV file with all decision records matching filters.
REQ-DASH-030
All API endpoints MUST implement rate limiting at 60 requests per minute per IP, except search endpoints (10 req/min).

Security Requirements

REQ-DASH-031
All user inputs MUST be escaped using Jinja2 autoescaping to prevent XSS attacks.
REQ-DASH-032
Supabase queries MUST use parameterized statements via SDK methods (never string concatenation).
REQ-DASH-033
CORS middleware MUST restrict allowed origins to http://localhost:8092 only.
REQ-DASH-034
Application MUST NOT log sensitive memory content or contact details to stdout/files.
REQ-DASH-035
Docker container MUST run as non-root user with read-only filesystem except for /tmp.

Deployment Requirements

REQ-DASH-036
Dockerfile MUST use Python 3.12 slim image with multi-stage build for minimal image size.
REQ-DASH-037
Container MUST mount Qdrant URL, Supabase credentials, and Ollama URL via environment variables (not hardcoded).
REQ-DASH-038
Docker Compose MUST include services: dashboard, redis (for scroll context cache).
REQ-DASH-039
Application MUST include health check endpoint for Docker HEALTHCHECK directive (30s interval).
REQ-DASH-040
Startup MUST verify Qdrant connection, Supabase connection, and Ollama availability before accepting requests.

5. Prompt to Build It

Build a Memory Dashboard web UI for Claude's vector memory system.

## Tech Stack
- Backend: FastAPI (Python 3.12+), async/await handlers
- Frontend: HTMX (reactive updates), Chart.js (analytics), D3.js (knowledge graph)
- Data: Qdrant (8 collections), Supabase (contacts table)
- Deployment: Docker, localhost-only (127.0.0.1:8092)

## Qdrant Collections (768-dim nomic-embed-text embeddings)
1. claude_memories (generic memories with type field)
2. learnings (extracted learnings)
3. trajectories (action sequences)
4. procedures (reusable processes)
5. episodes (conversation contexts)
6. memory_links (relationships: source_id → target_id, link_strength)
7. audit_log (governance decisions)
8. benchmarks (performance baselines)

## Common Metadata Schema
- type: learning | trajectory | procedure | episode | contact | decision | benchmark
- project: string (project identifier)
- tags: array of strings
- timestamp: ISO 8601
- sensitivity: public | internal | confidential | restricted
- recall_count: integer (incremented on recall)

## Features

### 1. Dashboard (/)
- Collection count cards (8 collections)
- Type distribution pie chart
- Top 10 projects bar chart
- Last 90 days timeline (line chart)
- Sensitivity breakdown donut chart
- Recent 10 memories table
- Auto-refresh every 30s (HTMX polling)

### 2. Memory Browser (/memories)
- Paginated table (50 per page, infinite scroll)
- Filters: type dropdown, project autocomplete, tags multi-select, date range, sensitivity radio
- Columns: Type badge, Content preview (80 chars), Project, Tags, Created, Recall Count, Sensitivity
- Click row: Expand full content + metadata JSON
- "View in Graph" button: Jump to knowledge graph centered on memory
- Qdrant scroll API with Redis-cached scroll contexts (5-min TTL)

### 3. Semantic Search (/search)
- Natural language query input
- Query → Ollama embedding (nomic-embed-text) → Qdrant vector search
- Settings: collections filter (checkboxes), score threshold slider (0.0-1.0), max results (50 default, 200 max), max sensitivity dropdown
- Results: Cards with content, similarity score, type, project, tags, "Why matched" snippet highlighting
- Sort: similarity score desc, then recall_count desc

### 4. Knowledge Graph (/graph)
- D3.js force-directed graph (max 500 nodes)
- Nodes: Colored by type (learnings=blue #60a5fa, trajectories=green #2dd4bf, procedures=purple #a78bfa, episodes=orange #fb923c)
- Edges: From memory_links, weighted by link_strength (0.0-1.0)
- Interactions: Hover tooltip, click to highlight neighbors, drag to reposition, zoom/pan
- Filters: Type checkboxes, project dropdown, link strength slider
- Layout modes: Force-directed (default), Hierarchical (Dagre), Radial (concentric by age)

### 5. Contacts View (/contacts)
- Query Supabase contacts table
- Columns: Name, Organization, Role, Email (masked: abc***@domain.com), Last Contact, Tags, Notes Preview
- Filters: Organization, Role, Tags, Date Range
- Click row: Full details + memory timeline (all memories mentioning contact)
- "Reveal" email button: Show full email, log to audit_log (event_type: email_reveal)

### 6. Decisions View (/decisions)
- Query audit_log collection
- Columns: Timestamp, Event Type, Decision, Context, Operator, Outcome
- Event types: human_gate, data_classification_gate, jurisdiction_check, approval_override, memory_redaction
- Filters: Event Type, Date Range, Operator, Outcome (approved/rejected/deferred)
- CSV export button

### 7. Analytics (/analytics)
- Daily memory creation line chart (6-month view)
- Type distribution stacked bar chart (per project)
- Sensitivity heatmap (type vs sensitivity, cells=count)
- Recall frequency histogram
- Tag cloud (sized by usage count)
- Embedding quality scatter plot (norm vs recall_count)

## API Endpoints

GET /health
→ {status, qdrant_ok, supabase_ok, ollama_ok}

GET /api/stats
→ {collection_counts, type_distribution, top_projects, timeline_90d, sensitivity_breakdown}

GET /api/memories?type=&project=&tags=&after=&before=&sensitivity=&page=&limit=
→ {memories: [...], total, page, has_more}

POST /api/search
{query, collections, score_threshold, max_results, max_sensitivity}
→ {results: [{memory, score, snippet}, ...]}

GET /api/graph-data?types=&project=&min_link_strength=&max_nodes=
→ {nodes: [{id, type, project, recall_count, ...}], edges: [{source, target, strength, ...}]}

GET /api/contacts?org=&role=&tags=&after=&before=
→ {contacts: [...], total}

POST /api/contacts/:id/reveal-email
→ {email} + audit_log entry

GET /api/decisions?event_type=&after=&before=&operator=&outcome=
→ {decisions: [...], total}

GET /api/export/decisions (CSV download)

## Security
- Bind to 127.0.0.1:8092 only (no external exposure)
- Jinja2 autoescaping (XSS prevention)
- Parameterized Supabase queries (SQL injection prevention)
- SRI hashes on Chart.js, D3.js, HTMX CDN scripts
- CORS restricted to localhost:8092
- Rate limiting: 60 req/min default, 10 req/min for search
- No sensitive content in logs
- Docker: non-root user, read-only filesystem except /tmp

## Environment Variables
QDRANT_URL=http://localhost:6333
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-anon-key
OLLAMA_URL=http://localhost:11434
REDIS_URL=redis://localhost:6379

## Docker Compose
services:
  dashboard:
    build: .
    ports: [127.0.0.1:8092:8092]
    environment: [QDRANT_URL, SUPABASE_URL, SUPABASE_KEY, OLLAMA_URL, REDIS_URL]
    healthcheck: [GET /health, 30s interval]
  redis:
    image: redis:7-alpine
    ports: [6379:6379]

## Deliverables
1. FastAPI app with all 7 views + 9 API endpoints
2. HTMX templates with dark glassmorphism styling
3. Chart.js configurations for 6+ chart types
4. D3.js force graph with 3 layout modes
5. Dockerfile (Python 3.12 slim, multi-stage)
6. docker-compose.yml
7. README with setup, API docs, screenshots

## Design Requirements
- Dark theme (#0f0f0f background, #f5f5f5 text)
- Glassmorphism cards (rgba(255,255,255,0.03) bg, 0.08 border)
- Gradient accents (#2dd4bf → #60a5fa)
- JetBrains Mono for code/IDs
- Responsive layout (mobile-friendly)
- Loading skeletons during HTMX requests
- Error toasts for failed API calls

Build this as a production-ready localhost dashboard for memory system visibility and debugging.

6. Design Decisions

Why HTMX Instead of React/Vue?

Decision: Use HTMX for frontend reactivity instead of a JavaScript framework.

Rationale:

Trade-offs: HTMX is less suitable for highly interactive UIs with complex client-side logic. Knowledge graph uses D3.js directly for this reason.

Why Redis for Scroll Context Caching?

Decision: Cache Qdrant scroll contexts in Redis with 5-minute TTL.

Rationale:

Alternative Considered: Store scroll context in session cookies. Rejected due to cookie size limits (4KB) and inability to share contexts across browser tabs.

Why Localhost-Only Deployment?

Decision: Bind to 127.0.0.1:8092 with no authentication layer.

Rationale:

Future Extension: If remote access needed, deploy behind Tailscale/NetBird ZTNA mesh with mTLS, not password auth.

Why Ollama for Embeddings Instead of OpenAI?

Decision: Use Ollama's nomic-embed-text model for search embeddings.

Rationale:

Trade-off: nomic-embed-text is 768-dim vs OpenAI's 1536-dim, potentially lower search quality. Acceptable for localhost debugging tool.

Why D3.js for Knowledge Graph Instead of Cytoscape?

Decision: Use D3.js force-directed graph instead of Cytoscape.js.

Rationale:

Alternative Considered: Cytoscape.js has better default styling and graph analysis algorithms, but overkill for visualization-only use case.

Why Limit Knowledge Graph to 500 Nodes?

Decision: Hard cap knowledge graph at 500 nodes, sorted by recall_count desc.

Rationale:

Future Extension: Implement WebGL rendering with force-graph library for 10K+ node support.

Why Email Masking with Audit Log?

Decision: Mask contact emails by default, require "Reveal" button click that logs to audit_log.

Rationale:

Implementation: Masking uses pattern first3chars***@domain on client side, full email stored in Supabase.

7. Integration Points

Qdrant Vector Database

Connection: HTTP client via qdrant-client Python library.

Operations:

# Example scroll query with filters from qdrant_client import QdrantClient from qdrant_client.models import Filter, FieldCondition, MatchValue client = QdrantClient(url=os.getenv("QDRANT_URL")) response = client.scroll( collection_name="claude_memories", limit=50, with_payload=True, with_vectors=False, scroll_filter=Filter( must=[ FieldCondition(key="type", match=MatchValue(value="learning")), FieldCondition(key="project", match=MatchValue(value="governance-plugin")) ] ) ) memories = [point.payload for point in response[0]] next_offset = response[1] # Cache in Redis

Supabase Contacts Table

Connection: supabase-py SDK with API key authentication.

Schema:

CREATE TABLE contacts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, organization TEXT, role TEXT, email TEXT, last_contact TIMESTAMP, tags TEXT[], notes TEXT, created_at TIMESTAMP DEFAULT NOW() );

Query Example:

from supabase import create_client supabase = create_client( os.getenv("SUPABASE_URL"), os.getenv("SUPABASE_KEY") ) response = supabase.table("contacts")\ .select("*")\ .eq("organization", "Acme Corp")\ .gte("last_contact", "2026-01-01")\ .order("last_contact", desc=True)\ .execute() contacts = response.data

Ollama Embeddings API

Endpoint: POST http://localhost:11434/api/embeddings

Request/Response:

# Request { "model": "nomic-embed-text", "prompt": "Docker networking issues on Ubuntu" } # Response { "embedding": [0.123, -0.456, 0.789, ...] # 768-dim array }

Error Handling: 10-second timeout, retry once on connection error, fallback to empty results if Ollama unavailable.

Redis Scroll Context Cache

Connection: redis-py async client.

Key Pattern: scroll:<collection>:<filters_hash>:<page>

import redis.asyncio as redis import hashlib import json async def cache_scroll_context(collection, filters, page, offset): r = redis.from_url(os.getenv("REDIS_URL")) filters_hash = hashlib.md5(json.dumps(filters, sort_keys=True).encode()).hexdigest() key = f"scroll:{collection}:{filters_hash}:{page}" await r.setex(key, 300, offset) # 5-minute TTL async def get_scroll_context(collection, filters, page): r = redis.from_url(os.getenv("REDIS_URL")) filters_hash = hashlib.md5(json.dumps(filters, sort_keys=True).encode()).hexdigest() key = f"scroll:{collection}:{filters_hash}:{page}" return await r.get(key)

Governance Plugin Audit Log

Integration: Dashboard writes to audit_log collection when sensitive actions occur.

Events Logged:

# Log email reveal await qdrant_client.upsert( collection_name="audit_log", points=[{ "id": str(uuid.uuid4()), "vector": [0.0] * 768, # Dummy vector "payload": { "event_type": "email_reveal", "timestamp": datetime.utcnow().isoformat(), "operator": "dashboard_user", "context": {"contact_id": contact_id, "email": email}, "outcome": "approved" } }] )

Memory System MCP Tools

Parallel Access: Dashboard and MCP tools (memory_recall, memory_store) access the same Qdrant collections.

Consistency Guarantees:

Schema Compatibility: Dashboard expects standard metadata fields (type, project, tags, timestamp, sensitivity, recall_count). Gracefully handles missing fields with defaults.