Skip to content

feat(docker): add multi-stage Dockerfile and docker-compose for Windows#24

Open
hoklims wants to merge 1857 commits into
tombelieber:mainfrom
hoklims:feat/docker-support
Open

feat(docker): add multi-stage Dockerfile and docker-compose for Windows#24
hoklims wants to merge 1857 commits into
tombelieber:mainfrom
hoklims:feat/docker-support

Conversation

@hoklims

@hoklims hoklims commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add multi-stage Dockerfile (Bun frontend + Node sidecar + Rust backend + Debian slim runtime)
  • Add docker-compose.yml with Windows-compatible volume mounts (%USERPROFILE%/.claude)
  • Add .dockerignore for fast build context

Motivation

claude-view currently only runs on macOS. This adds Docker support so users on Windows (and Linux) can run the full dashboard via Docker Desktop with a single docker compose up.

Verified working on Windows 11 with Docker Desktop — indexes 463 sessions, live monitor, analytics, and Claude CLI detection all functional.

Architecture

Stage 1 (frontend)  → oven/bun:latest      → vite build → dist/
Stage 2 (sidecar)   → node:22-slim         → tsup build → dist/
Stage 3 (backend)   → rust:1.88-slim       → cargo build --release
Stage 4 (runtime)   → debian:bookworm-slim → binary + assets + Claude CLI

docker-compose mounts:

  • ~/.claude/root/.claude (session data, r/w for hook registration)
  • Named volume claude-view-data/data (DB, search index persistence)

Dependencies

Depends on #23 (CLAUDE_VIEW_SKIP_PLATFORM_CHECK and CLAUDE_VIEW_BIND_ADDR env vars).

Test plan

  • docker compose build succeeds (4-stage parallel build)
  • docker compose up starts server on port 47892
  • /api/health returns {"status":"ok"}
  • Sessions indexed from Windows host ~/.claude/projects/
  • Live Monitor displays active sessions with tasks and sub-agents
  • Analytics page renders with full history data
  • Claude CLI detected inside container (no "CLI Missing" warning)
  • Verify on Linux host (untested, should work)

🤖 Generated with Claude Code

tombelieber and others added 30 commits March 11, 2026 21:57
Scans all JSONL files under ~/.claude/projects/ at startup, building
a team_name → [TeamJSONLRef] index. Uses memmem SIMD pre-filter to
skip files without "teamName" in microseconds. One session may contain
multiple teams (nvda-demo + python-vs-go-demo pattern verified in real data).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…SessionHit

- Add `engines: Vec<String>` to `SessionHit` (e.g. ["tantivy"], ["grep"])
- Remove response-level `search_engine: Option<String>` from `SearchResponse`
- Update all SessionHit constructors in query.rs (tantivy) and unified.rs (grep)
- Update generated TS types: SearchResponse.ts drops searchEngine, SessionHit.ts gains engines
- Update SearchResults.tsx to derive grep indicator from session.engines per-session
- Update CommandPalette.search.test.tsx assertions to match new per-session engines shape
- All 8 unified:: unit tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…SessionHit

- Add `engines: Vec<String>` to `SessionHit` (e.g. ["tantivy"], ["grep"])
- Remove response-level `search_engine: Option<String>` from `SearchResponse`
- Update all SessionHit constructors in query.rs (tantivy) and unified.rs (grep)
- Update generated TS types: SearchResponse.ts drops searchEngine, SessionHit.ts gains engines
- Update SearchResults.tsx to derive grep indicator from session.engines per-session
- Update CommandPalette.search.test.tsx assertions to match new per-session engines shape
- All 8 unified:: unit tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reconstructs TeamDetail from JSONL by scanning for:
- TeamCreate tool_use → team name + description
- Agent/Task spawns with team_name match → member list
- First timestamp → created_at

Uses SIMD memmem pre-filter on team name. Agent spawns without
team_name (regular sub-agents) are correctly excluded. Colors
are deterministic (hash of member name → palette index).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reconstructs TeamDetail from JSONL by scanning for:
- TeamCreate tool_use → team name + description
- Agent/Task spawns with team_name match → member list
- First timestamp → created_at

Uses SIMD memmem pre-filter on team name. Agent spawns without
team_name (regular sub-agents) are correctly excluded. Colors
are deterministic (hash of member name → palette index).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scans JSONL for SendMessage tool_use calls (team-lead → member outbound
messages). Uses dual memmem pre-filter (teamName + SendMessage) for
efficiency. Historical messages are always marked read: true.
Messages sorted chronologically by ISO timestamp string comparison.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scans JSONL for SendMessage tool_use calls (team-lead → member outbound
messages). Uses dual memmem pre-filter (teamName + SendMessage) for
efficiency. Historical messages are always marked read: true.
Messages sorted chronologically by ISO timestamp string comparison.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SearchPrefilter struct and Database::search_prefilter_session_ids()
to narrow the session set via SQLite before grep/Tantivy run. Uses the
polymorphic project filter pattern (project_id OR git_root) and maps
filter fields to actual schema columns (git_branch, primary_model).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SearchPrefilter struct and Database::search_prefilter_session_ids()
to narrow the session set via SQLite before grep/Tantivy run. Uses the
polymorphic project filter pattern (project_id OR git_root) and maps
filter fields to actual schema columns (git_branch, primary_model).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Tantivy)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Tantivy)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TeamsStore now holds a jsonl_index (TeamJSONLIndex) built at startup.
All three lookup methods fall back to JSONL reconstruction when a team
is missing from the filesystem:

- get(): filesystem → reconstruct_team_from_jsonl()
- inbox(): filesystem → reconstruct_inbox_from_jsonl()
- summaries(): includes JSONL-only teams in the sorted list

Filesystem always wins when a team exists on both (verified by
test_get_prefers_filesystem_over_jsonl).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TeamsStore now holds a jsonl_index (TeamJSONLIndex) built at startup.
All three lookup methods fall back to JSONL reconstruction when a team
is missing from the filesystem:

- get(): filesystem → reconstruct_team_from_jsonl()
- inbox(): filesystem → reconstruct_inbox_from_jsonl()
- summaries(): includes JSONL-only teams in the sorted list

Filesystem always wins when a team exists on both (verified by
test_get_prefers_filesystem_over_jsonl).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests verify co-primary search with both grep and Tantivy:
- Both engines find the same session (grep primary, Tantivy supplements)
- CJK queries fallback to grep when Tantivy insufficient
- Results properly sorted by recency (modified_at DESC)

All 3 tests pass in 0.12s.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Tests verify co-primary search with both grep and Tantivy:
- Both engines find the same session (grep primary, Tantivy supplements)
- CJK queries fallback to grep when Tantivy insufficient
- Results properly sorted by recency (modified_at DESC)

All 3 tests pass in 0.12s.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
… execution

- Add structured filter params to SearchQuery: project, branch, model, after, before
- Add parse_iso_date helper using chrono (already a workspace dep)
- Build SearchPrefilter from params (with scope backward compat for project: prefix)
- Run SQLite pre-filter when any structured filter is set, skip otherwise
- Collect JSONL files narrowed by session IDs from pre-filter
- Get search index as Option (no 503 — grep is primary, Tantivy supplements)
- Run unified_search via spawn_blocking; set elapsed_ms in handler, not engine
- Handler returns SearchResponse directly (result.response)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… execution

- Add structured filter params to SearchQuery: project, branch, model, after, before
- Add parse_iso_date helper using chrono (already a workspace dep)
- Build SearchPrefilter from params (with scope backward compat for project: prefix)
- Run SQLite pre-filter when any structured filter is set, skip otherwise
- Collect JSONL files narrowed by session IDs from pre-filter
- Get search index as Option (no 503 — grep is primary, Tantivy supplements)
- Run unified_search via spawn_blocking; set elapsed_ms in handler, not engine
- Handler returns SearchResponse directly (result.response)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
summaries() was reading each JSONL file twice per JSONL-only team —
once for reconstruct_team_from_jsonl, once for reconstruct_inbox_from_jsonl.
New reconstruct_team_and_inbox_from_jsonl() combines both in one pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
summaries() was reading each JSONL file twice per JSONL-only team —
once for reconstruct_team_from_jsonl, once for reconstruct_inbox_from_jsonl.
New reconstruct_team_and_inbox_from_jsonl() combines both in one pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Real Claude Code JSONL omits teamName on the TeamCreate assistant
message itself — the team name only appears inside input.team_name.
The previous code skipped these lines via the `line_team != team_name`
guard, so JSONL-only teams (nvda-demo, python-vs-go, etc.) never
appeared in /api/teams despite being indexed.

Fix: allow lines through if they contain a TeamCreate block (detected
via SIMD memmem pre-filter), and let the inner input.team_name check
confirm the match. Applies to both reconstruct_team_from_jsonl and
reconstruct_team_and_inbox_from_jsonl.

E2E verified: /api/teams now returns 17 teams (was 10), including all
7 JSONL-only teams that had their filesystem dirs deleted.

Regression tests: test_reconstruct_team_from_jsonl_without_toplevel_teamname
and test_reconstruct_combined_without_toplevel_teamname use real-world
message shapes (no teamName on TeamCreate line) to prevent recurrence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Real Claude Code JSONL omits teamName on the TeamCreate assistant
message itself — the team name only appears inside input.team_name.
The previous code skipped these lines via the `line_team != team_name`
guard, so JSONL-only teams (nvda-demo, python-vs-go, etc.) never
appeared in /api/teams despite being indexed.

Fix: allow lines through if they contain a TeamCreate block (detected
via SIMD memmem pre-filter), and let the inner input.team_name check
confirm the match. Applies to both reconstruct_team_from_jsonl and
reconstruct_team_and_inbox_from_jsonl.

E2E verified: /api/teams now returns 17 teams (was 10), including all
7 JSONL-only teams that had their filesystem dirs deleted.

Regression tests: test_reconstruct_team_from_jsonl_without_toplevel_teamname
and test_reconstruct_combined_without_toplevel_teamname use real-world
message shapes (no teamName on TeamCreate line) to prevent recurrence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…esults

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…esults

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tombelieber and others added 21 commits March 14, 2026 11:41
- All create tests now pass initialMessage (required for SDK to init)
- Multi-turn WS test replaced with HTTP /send endpoint test
- Fix biome noNonNullAssertion warnings (use optional chaining)
- 12/12 E2E tests pass against real Agent SDK with haiku

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sessions in waiting_permission state need user attention (permission card).
Without auto-connect, user navigates to the session but sees nothing —
they'd have to type a message first, which is nonsensical when the session
is waiting for THEIR input on a tool approval.

Matches VS Code / ChatGPT / Cursor pattern: auto-surface states that need
user attention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Silent failure hunter found: fire-and-forget sendMessage with .catch()
swallows errors. If sendMessage rejects (auth error, rate limit),
waitForSessionInit hangs for 15s then times out — the real error is lost.

Fix: Race both promises with Promise.all. If sendMessage rejects, the
create route fails fast with the actual error instead of a confusing
15s timeout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes from agent review team:

1. waitForSessionInit now guards against session_init firing with empty
   sessionId (if sdkSession.sessionId throws, the silent catch in
   updateSessionState leaves sessionId='' — handler keeps waiting instead
   of resolving with empty ID)

2. Create route cleans up orphaned sessions on failure — if
   waitForSessionInit rejects (timeout, fatal error, send failure),
   closeSession removes the ghost session from the registry instead
   of leaving it to accumulate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hardcoded model lists with the SDK's authoritative model list.
The sidecar fetches models via V1 query().initializationResult() on
startup (refreshed hourly), caches them in memory, and serves via
GET /supported-models. The Rust backend proxies this to the frontend.

ModelSelector priority chain: SDK → /api/models → hardcoded fallback.

Includes timeout protection (30s), interrupt()-based cleanup, retry:false
on the frontend hook, and all findings from code review addressed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the single blocking `claude plugin marketplace update` call with
parallel per-marketplace CLI calls, fixing the 30s timeout and adding
real-time per-row status in the Marketplaces dialog.

Backend:
- New MarketplaceRefreshTracker with Mutex<HashMap> state, 5-min
  staleness guard, 30s TTL eviction (10 unit tests)
- POST /refresh-all spawns parallel tokio tasks (60s timeout each),
  orchestrator holds MARKETPLACE_LOCK for the batch
- GET /refresh-status returns per-marketplace status snapshot
- Add timeout_secs param to run_claude_plugin_in (existing callers
  unchanged at 30s)
- Make get_marketplace_lock pub(crate), remove dead bulk update path

Frontend:
- useMarketplaceRefresh hook with 1s polling, completion toast, cache
  invalidation
- Per-row status: queued (pulse), running (ping), failed (retry button)
- Fix dialog centering to Tailwind classes per CLAUDE.md

Empirically validated: 7 parallel updates complete in ~6s vs ~28s serial.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	crates/server/src/lib.rs
#	crates/server/src/routes/jobs.rs
#	crates/server/src/routes/terminal.rs
#	crates/server/src/state.rs
…calls

Race condition: when multiple frontend requests arrive simultaneously
during sidecar startup, the first caller spawns the process and polls
the health endpoint, but concurrent callers see "child alive" and return
the socket path immediately — before the sidecar has started listening.
This causes "No such file or directory" connection errors.

Fix: check socket file existence before returning "ready". If child is
alive but socket doesn't exist yet, wait for health check (same loop
as the spawner) instead of returning immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Redesign ModelSelector dropdown to show model name, description
(from SDK), and context window size — matching the reference design.
Wider dropdown (w-72), two-line layout per item, check mark for
active selection, "Select a model" header.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The formatContextWindow() helper returned '200K' for all models, but
Opus 4.6 is actually 1M context. Remove fake data rather than show
wrong data. Context window display needs real data piped through from
LiteLLM (max_input_tokens field) — tracked as follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ring

Tailwind v4 `-translate-x-1/2 -translate-y-1/2` generates the CSS `translate`
property instead of `transform`, which breaks centering when composed with
Radix internals or animations. All 8 dialogs now use `DialogContent` /
`AlertDialogContent` wrappers that bake in inline `transform: translate(-50%,
-50%)` — immune to TW v4 composability issues. Zero raw Dialog.Content /
AlertDialog.Content remains outside the wrapper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace generic gray rectangles with structured skeletons that match real
component layouts (gauge cards, session rows, process rows). Add inline
skeleton placeholders for process-tree-dependent data (chevrons, CLI/IDE
badges, proc counts, child proc hints, orphaned processes) so the UI
shows the final layout shape while the process tree is still loading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ata source

Memory gauge showed 68.7 GB instead of 64 GB because formatBytes used
decimal (÷1e9) instead of binary (÷1024³) units. Active Sessions gauge
was always red (value/max=100%) and showed a different count than the
Claude Sessions panel due to two independent SSE streams.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use node:22-slim as runtime base instead of debian:bookworm-slim to
  match sidecar's node20+ target and avoid Node version mismatch
- Pin @anthropic-ai/claude-code@2.1.76 for reproducible builds
- Remove redundant second sed for LTO override (COPY crates/ doesn't
  overwrite root Cargo.toml)
- Make docker-compose.yml cross-platform: use CLAUDE_HOME_DIR env var
  with $HOME fallback instead of Windows-only $USERPROFILE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ck ordering

- Auto-resume: dormant sessions (no controlId) now call /api/control/sessions/resume
  before opening WS, enabling send-from-any-state without manual reconnect
- Fix optimistic block timeline: user messages appear before assistant response,
  not appended after stream blocks
- Status lifecycle: 'sent' now clears to undefined (block stays visible) instead
  of being removed; only 'optimistic'/'sending' blocks timeout to 'failed'
- deriveEffectiveSend/deriveCanResumeLazy updated to treat sessionId alone as
  resumable (not just controlId)
- session_closed clears controlId + resets lastSeq for clean next-connect
- Feature flag: FEATURES.chat=true in dev only, false in prod builds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes Clippy unnecessary_sort_by warnings across core, search, and db
crates by using sort_by_key with std::cmp::Reverse.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
promptId (UUID) appears on type:"user" JSONL records linking user messages
to saved prompt templates. 16,411 occurrences in real data. Field is real
but not consumed by the parser pipeline — added to intentionally_ignored so
the evidence audit passes without extracting unused data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add platform abstraction layer (platform.rs) that replaces Unix-specific
code with cross-platform equivalents:

- Process management: libc::kill → taskkill on Windows, libc on Unix
- IPC: Unix domain sockets → TCP on Windows for sidecar communication
- Sidecar (Node.js): detect TCP vs Unix socket mode via SIDECAR_SOCKET
- Make libc dependency Unix-only in Cargo.toml
- Fix all sort_by → sort_by_key Clippy warnings across server crate
- Fix unused variable warnings in time_range.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add crates/db/src/queries/presets.rs with full CRUD operations for the
presets and preset_state tables: list, get, create, update metadata,
delete, and state management (set active, clear, set active only).
Includes 6 passing tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge main's socket readiness check and model cache refresh into
Docker/Windows TCP support branch. Added is_addr_ready() helper to
handle both Unix socket and TCP address modes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tombelieber

tombelieber commented Mar 19, 2026

Copy link
Copy Markdown
Owner

Thanks! Would it be possible to decouple Docker and Windows support for now? The app is currently tightly bound to macOS, and I'm concerned Windows might introduce stability issues we're not equipped to handle at this stage.

hoklims and others added 2 commits March 19, 2026 12:43
Decouple Docker support from Windows support per maintainer request.
Remove platform.rs, revert sidecar to Unix socket only, restore
libc::kill/SIGTERM calls, remove TCP dual-mode from sidecar TS.

Docker-only changes (Dockerfile, docker-compose, env vars) are preserved.
Windows support will be submitted as a separate PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strip all Windows runtime support (taskkill, sysinfo, TCP sidecar mode)
from platform.rs. Keep only Unix implementations with cfg(not(unix))
compile stubs so the crate builds on all platforms but only runs on
macOS/Linux (enforced by the platform gate in main.rs).

Docker-only changes (Dockerfile, docker-compose, env vars) are preserved.
Windows support will be submitted as a separate PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hoklims

hoklims commented Mar 19, 2026

Copy link
Copy Markdown
Contributor Author

Makes sense, you're right that bundling Windows into this was too much at once. I've pushed a cleanup — all the Windows runtime stuff is gone now (the taskkill/sysinfo abstractions, TCP sidecar mode, the whole dual-mode plumbing).

What's left is purely Docker: the Dockerfile, compose file, and the two env vars needed to run in a Linux container (SKIP_PLATFORM_CHECK and BIND_ADDR). The platform.rs file is still there but it's just Unix implementations with compile stubs — no Windows runtime code.

Also addressed the Copilot remarks along the way: claude-code was already pinned, runtime Node matches the sidecar build target, and the compose file now defaults to $HOME instead of $USERPROFILE.

Happy to adjust if anything else stands out.

@tombelieber

Copy link
Copy Markdown
Owner

@hoklims Great thx, will do a review and merge this week. Thx for the contribution.

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.

3 participants