Data Browser
The Data Browser is a web-based file explorer for viewing all Vita health data files. It provides a resizable split-pane interface with collapsible folder navigation, JSON syntax highlighting, and JSONL pagination for large log files.
Overview
The Data Browser exposes structured health data through a secure API that enforces strict path validation (10-step defense-in-depth) to prevent directory traversal attacks. Only whitelisted directories are accessible: data, profile, goals, conditions, trends, insights, and meta.
Why it exists: Debugging health data and understanding system state requires browsing raw JSON files. Rather than using terminal commands, the Data Browser provides a visual interface with syntax highlighting, search, and pagination for large JSONL files.
How to Access
- Navigate to
/datain the web UI - The sidebar displays a file tree of all allowed directories
- Click any file to view its contents in the main panel
User Interface
+-------------------------+--------------------------------------------------+
| | |
| SIDEBAR (resizable) | CONTENT AREA |
| 200px - 480px | |
| +---------------------+ | +----------------------------------------------+ |
| | [Search files...] | | | Home > data > checkins > 2026-01-03.json | |
| +---------------------+ | +----------------------------------------------+ |
| [+ Expand] [- Collapse] | |
| | +----------------------------------------------+ |
| v data/ (12) | | [File] 2026-01-03.json [JSON] | |
| v checkins/ (31) | | [Copy] [Download] | |
| 2026-01-01.json | +----------------------------------------------+ |
| 2026-01-02.json | |
| 2026-01-03.json <- | | 1 { | |
| > garmin/ (31) | | 2 "mood": 8, | |
| > identity/ (4) | | 3 "energy": 7, | |
| v profile/ (3) | | 4 "sleep_quality": "good" | |
| basics.json | | 5 } | |
| preferences.json | |
| communication.json | +----------------------------------------------+ |
| | | [<<] [<] [1] [2] ... [10] [>] [>>] | |
| ----------------------- | | (JSONL pagination only) | |
| 7 root folders | +----------------------------------------------+ |
+-------------------------+--------------------------------------------------+
^ ^
| |
Drag handle Floating back button (mobile)
(desktop only) + bottom action bar (mobile)
Data Flow
User clicks file in tree
|
v
+------------------+ +------------------+ +------------------+
| TanStack Router | -----> | useDataFile hook | -----> | API: GET /file/ |
| updates URL | | (React Query) | | {path}?offset=0 |
+------------------+ +------------------+ +------------------+
|
v
+------------------+
| FileViewer |
| - Formats JSON |
| - Highlights |
| - Paginates |
+------------------+
Layout Components
| Component | Description | Behavior |
|---|---|---|
| DataBrowserLayout | Split-pane container | Manages sidebar width state, drag-resize handling |
| FileTree | Collapsible file tree | Default expands first level, memoized for performance |
| FileViewer | Code viewer with actions | Breadcrumbs, syntax highlighting, copy/download |
| Resize Handle | Drag divider (desktop) | Min 200px, max 480px, cursor changes during drag |
Key UI Features
| Feature | Implementation | Why |
|---|---|---|
| Real-time search | matchesSearch() recursive filter |
Find files without scrolling |
| Auto-expand on search | effectiveExpanded set from all dirs |
Show matches in context |
| Memoized tree nodes | React.memo(TreeNode) |
Prevent re-renders on sibling clicks |
| Lazy child rendering | Only render children when expanded | Large trees stay fast |
| JSON syntax highlighting | String replacement with HTML spans | No external dependencies |
| Copy with feedback | 2-second checkmark + "Copied!" text | Confirm action succeeded |
Visual Elements
- Folder icons: Closed folder (indigo-400), Open folder (indigo-500)
- File badges:
JSON(gold gradient),JSONL(blue gradient) - Active file: Indigo left border + gradient background
- Child count: Hover reveals item count per folder (visible on group-hover)
- Skeleton loading: Shimmer animation while fetching tree or file
Mobile Behavior
On screens below the md breakpoint (768px):
- Sidebar: Full width, collapses to show file list only
- Floating back button: Fixed position top-left to return to tree
- Bottom action bar: Copy/Download buttons in fixed footer
- Pagination: Shows
X / Ycompact format instead of page numbers - Tree items: Taller (48px) for touch targets
Data Sources Exposed
| Directory | Contents | File Format |
|---|---|---|
data/checkins/ |
Daily health check-ins | JSON per day |
data/garmin/ |
Synced Garmin Connect data (sleep, HRV, steps) | JSON per day |
data/rwgps/ |
RideWithGPS cycling data | JSON per sync |
data/identity/ |
Observations, interviews, quotes | JSONL append-only |
data/proactive/ |
Telegram message queue | SQLite (not browsable) + JSONL |
data/scheduling/ |
Frequency goal occurrences | JSONL per month |
data/tokens/ |
Token usage aggregates, sessions, summaries | JSON/JSONL |
data/board/ |
Board meeting data and resolutions | JSON per meeting |
data/objectives/ |
Daily and weekly objectives | JSON per day/week |
data/experiments/ |
Experiment protocols and results | JSON per experiment |
data/media/ |
Media consumption (HN, RSS, podcasts, X) | JSONL per day |
data/location/ |
Location history | JSONL per day |
data/activities/ |
Unified activity data (cycling, runs, etc.) | JSONL per year |
data/coaching/ |
Coaching effectiveness and cooldowns | JSON/JSONL |
profile/ |
User settings (basics, preferences, communication) | JSON |
goals/active/ |
Current health goals | JSON per goal |
goals/achieved/ |
Completed goals archive | JSON per goal |
insights/ |
AI-generated health insights | JSON |
meta/ |
System metadata, feature intents | JSON/JSONL |
trends/ |
Computed trend data | JSON |
conditions/ |
Health conditions tracking | JSON |
Architecture
Component Hierarchy
/data (index.tsx) /data/{path} ($.tsx)
| |
v v
+------------------+ +------------------+
| DataBrowserLayout| | DataBrowserLayout|
+------------------+ +------------------+
| |
+---------+ +-----------+-----------+
| | | | |
FileTree WelcomeContent FileTree FileViewer (skeletons)
State Management
The Data Browser uses TanStack Query for server state with these caching strategies:
| Query | Stale Time | Why |
|---|---|---|
Tree (/api/v1/data/tree) |
5 minutes | Directory structure rarely changes |
| File content | 30 seconds | Files may be updated by check-ins/syncs |
| File metadata | 5 minutes | Line counts stable unless file appended |
Local state is minimal:
- sidebarWidth in DataBrowserLayout (drag resize)
- expandedPaths in FileTree (folder collapse state)
- searchQuery in FileTree (filter text)
- offset in route pages (pagination)
File Locations
| Component | Path | Responsibility |
|---|---|---|
| Route: Index | web/src/routes/data/index.tsx |
Welcome screen, tree only |
| Route: File View | web/src/routes/data/$.tsx |
Tree + file viewer |
| Layout | web/src/components/data/DataBrowserLayout.tsx |
Split pane, resize logic |
| File Tree | web/src/components/data/FileTree.tsx |
Navigation, search, expand/collapse |
| File Viewer | web/src/components/data/FileViewer.tsx |
Display, highlight, paginate |
| Index | web/src/components/data/index.ts |
Barrel exports |
| Hooks | web/src/hooks/useDataBrowser.ts |
API integration (3 hooks) |
| API Router | api/src/routers/data.py |
Path validation, file reading |
| Service | api/src/services/data_browser.py |
Tree building, content extraction |
| Schemas | api/src/schemas/data_browser.py |
Pydantic models |
Data Hooks
// Fetch entire tree structure (cached 5 min)
useDataTree(): { data: DataTreeResponse, isLoading, error }
// Fetch file content with pagination (cached 30s)
useDataFile(path, offset, limit): { data: DataFileResponse, isLoading, error }
// Fetch metadata only (for line counts without content)
useDataFileMeta(path): { data: DataFileMeta, isLoading, error }
API Endpoints
| Method | Endpoint | Description | Response |
|---|---|---|---|
| GET | /api/v1/data/tree |
Directory tree of allowed roots | DataTreeResponse |
| GET | /api/v1/data/file/{path} |
File contents with optional pagination | DataFileResponse |
| GET | /api/v1/data/file/{path}/meta |
Metadata without content | DataFileMeta |
Tree Response Schema
{
"roots": [
{
"name": "data",
"type": "dir",
"children": [
{
"name": "checkins",
"type": "dir",
"children": [
{
"name": "2026-01-03.json",
"type": "file",
"size": 1234,
"modified": 1704326400.0
}
]
}
]
}
]
}
File Response Schema
{
"path": "data/checkins/2026-01-03.json",
"content": { "mood": 8, "energy": 7 },
"is_jsonl": false
}
For JSONL files:
{
"path": "data/identity/observations.jsonl",
"content": [
{ "id": "obs-001", "text": "..." },
{ "id": "obs-002", "text": "..." }
],
"is_jsonl": true,
"total_lines": 847,
"offset": 0,
"limit": 100
}
Query Parameters
| Param | Type | Default | Range | Description |
|---|---|---|---|---|
offset |
int | 0 | >= 0 | Starting line for JSONL pagination |
limit |
int | 100 | 1-1000 | Lines per page |
Security Model
The Data Browser implements defense-in-depth with a 10-step path validation:
- Reject empty input
- Decode URL encoding (with warning log)
- Reject backslashes
- Use PurePosixPath for consistent parsing
- Reject absolute paths
- Check for
..traversal BEFORE resolution - Verify first component in
ALLOWED_ROOTS - Join with base and resolve
- Verify resolved path still under base
- Block symlinks in any component
Constants
| Constant | Value | Purpose |
|---|---|---|
ALLOWED_ROOTS |
data, profile, goals, conditions, trends, insights, meta | Whitelisted directories |
MAX_FILE_SIZE |
10 MB | Prevent memory exhaustion |
MAX_JSONL_LINES |
1000 | Max lines per request |
MAX_TREE_DEPTH |
10 | Prevent deep recursion |
Code Patterns
JSON Syntax Highlighting
The highlightJSONToHTML() function uses regex replacement instead of a parsing library:
// Pattern: escape HTML, then apply spans for keys, strings, numbers, booleans, null
const escaped = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
return escaped
.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/g, '<span class=\'json-key\'>"$1"</span>:')
.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"/g, '<span class=\'json-string\'>"$1"</span>')
// ... numbers, booleans, null
Why regex instead of a parser? Zero dependencies, smaller bundle, fast enough for typical file sizes. The trade-off is no semantic understanding (can't distinguish nested arrays from top-level).
Memoized Tree Rendering
TreeNode is wrapped in React.memo() to prevent re-renders when siblings change:
const TreeNode = memo(function TreeNode({ node, path, ... }) {
// Only renders children when expanded (lazy rendering)
{isExpanded && (
<div className="tree-children">
{visibleChildren.map((child) => <TreeNode key={child.name} ... />)}
</div>
)}
});
Drag-Resize Pattern
DataBrowserLayout handles resize with mouse events:
const handleMouseDown = useCallback((e) => {
e.preventDefault();
setIsDragging(true);
}, []);
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e) => {
const newWidth = e.clientX - containerRect.left;
setSidebarWidth(Math.max(200, Math.min(480, newWidth)));
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => { /* cleanup */ };
}, [isDragging]);
Why document listeners? Mouse can move outside the handle during drag. The overlay <div className="fixed inset-0"> prevents text selection.
Known Limitations
| Limitation | Impact | Workaround |
|---|---|---|
| Read-only | Cannot edit files in browser | Use CLI or API for writes |
| No binary files | Images, SQLite not viewable | Open in native apps |
| O(n) line counting | Slow metadata for huge JSONL | Pre-compute line counts |
| No content search | Can only filter by filename | Use grep in terminal |
| No diff view | Cannot see file history | Use git log/diff |
| No keyboard nav | Mouse-only tree navigation | Planned improvement |
| URL-encoded paths | Special chars need encoding | Handled in encodePath() |
Improvement Opportunities
These are prioritized by estimated impact and implementation complexity:
High Value, Low Effort
- Keyboard navigation - Arrow keys for tree,
Cmd+Cfor copy,Cmd+Fto focus search - Add
onKeyDownhandler to FileTree - Track
focusedPathin state -
Effort: ~2 hours
-
Persist sidebar width - Store in localStorage
- Read on mount, write on mouseup
-
Effort: ~30 minutes
-
Dark mode syntax theme - Already have CSS classes
- Add
.dark .json-key { color: #... }rules - Effort: ~1 hour
Medium Value, Medium Effort
- Content search - Full-text search across files
- Add
/api/v1/data/search?q=endpoint using grep - Display results with file path + matching lines
-
Effort: ~4 hours
-
Lazy tree loading - Fetch children on expand
- Add
/api/v1/data/tree/{path}for subdirectory - Initial render just top-level
-
Effort: ~3 hours
-
Schema validation warnings - Highlight malformed JSON
- Parse with try/catch, show error banner
- Effort: ~2 hours
High Value, High Effort
- Edit mode - Inline JSON editing with validation
- ContentEditable or Monaco editor
- POST to new
/api/v1/data/file/{path}endpoint - Use atomic_write() for safety
-
Effort: ~8 hours
-
Git diff viewer - Compare with previous versions
- Run
git show HEAD~1:{path}via API - Side-by-side diff view
-
Effort: ~6 hours
-
Real-time updates - WebSocket for file changes
- Watch filesystem with inotify
- Push updates to connected clients
-
Effort: ~8 hours
-
Bulk export - Download multiple files as ZIP
- Select files with checkboxes
- Stream ZIP from API
- Effort: ~4 hours