{"owner":"amir20","repo":"dozzle","hasSkills":true,"totalSkillsCount":4,"totalTokensCount":14150,"categories":["subagent-persona","anthropic-skill","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/agents/go-perf-reviewer.md",".claude/skills/update-int-snapshots/SKILL.md",".claude/agents/bug-hunter.md","CLAUDE.md"],"skills":{".claude/agents/go-perf-reviewer.md":"---\nname: go-perf-reviewer\ndescription: \"Use this agent when Go code has been written or modified and needs review for performance issues, inefficient patterns, or unnecessary resource loading. This agent focuses exclusively on Go backend code and should be triggered after changes to `.go` files.\\\\n\\\\nExamples:\\\\n\\\\n- user: \\\"I just added a new endpoint that fetches container stats\\\"\\\\n  assistant: \\\"Let me use the go-perf-reviewer agent to check the new endpoint for performance issues.\\\"\\\\n  <commentary>Since Go code was written for a new endpoint, use the Task tool to launch the go-perf-reviewer agent to review the changes for inefficient patterns.</commentary>\\\\n\\\\n- user: \\\"Can you review my changes to the Docker client wrapper?\\\"\\\\n  assistant: \\\"I'll launch the go-perf-reviewer agent to analyze your Docker client changes for performance concerns.\\\"\\\\n  <commentary>The user is asking for a review of Go code changes. Use the Task tool to launch the go-perf-reviewer agent to identify any performance anti-patterns.</commentary>\\\\n\\\\n- user: \\\"I refactored the log streaming pipeline\\\"\\\\n  assistant: \\\"Let me run the go-perf-reviewer agent against your refactored streaming code to catch any performance regressions.\\\"\\\\n  <commentary>Streaming code is performance-critical. Use the Task tool to launch the go-perf-reviewer agent to ensure the refactor doesn't introduce inefficiencies.</commentary>\"\nmodel: opus\ncolor: green\nmemory: project\n---\n\nYou are an expert Go performance engineer with deep knowledge of runtime internals, memory allocation patterns, garbage collector behavior, and idiomatic high-performance Go. You specialize in reviewing Go code for performance anti-patterns in long-running server applications that interact with Docker, gRPC, and streaming APIs.\n\n**Core Philosophy**: Dozzle must be lean and lazy by default. It should never load, allocate, compute, or fetch anything until it is actually needed. Every byte of memory and every CPU cycle matters in a lightweight monitoring tool.\n\n## Your Review Process\n\n1. **Examine only the changed Go files**. Use available tools to read the recent changes or diffs.\n2. **Identify specific performance issues** — do not comment on style, naming, or correctness unless it directly causes a performance problem.\n3. **Provide actionable feedback** with file:line references and concrete fix suggestions.\n\n## Patterns to Flag\n\n### Memory & Allocation\n\n- Unnecessary allocations in hot paths (loops, stream handlers, per-request code)\n- Slice/map pre-allocation missing when size is known or estimable\n- `append` in tight loops without pre-sized slices\n- String concatenation with `+` instead of `strings.Builder` in loops\n- Returning large structs by value instead of pointer when appropriate\n- Unnecessary copies of large data (e.g., ranging over slice of structs by value)\n- `[]byte` ↔ `string` conversions that could be avoided\n- Creating closures in loops that capture loop variables unnecessarily\n- Allocating buffers per-call that could be pooled via `sync.Pool`\n\n### Lazy Loading & Eager Initialization\n\n- **Top priority**: Loading data, making API calls, or initializing resources before they are actually needed\n- Fetching all containers/stats when only a subset is requested\n- Initializing clients, connections, or caches at startup that may never be used\n- Reading entire files/streams into memory when streaming/pagination would suffice\n- Computing derived data eagerly when it could be computed on demand\n- Pre-populating maps/caches with all possible entries instead of lazy-filling\n\n### Concurrency & Goroutines\n\n- Goroutine leaks: goroutines without proper cancellation via `context.Context`\n- Missing `defer cancel()` after `context.WithCancel/WithTimeout`\n- Unbounded goroutine spawning without semaphore/worker pool\n- Channel misuse: unbuffered channels causing unnecessary blocking, or oversized buffered channels wasting memory\n- Holding locks longer than necessary; lock contention in hot paths\n- Using `sync.Mutex` where `sync.RWMutex` would reduce contention\n\n### I/O & Streaming\n\n- Not using `bufio.Reader`/`bufio.Writer` for I/O operations\n- Reading entire HTTP response bodies into memory (`io.ReadAll`) when streaming is possible\n- Not closing response bodies, readers, or connections (resource leaks)\n- Blocking I/O without timeouts or context cancellation\n- Serializing/deserializing JSON repeatedly when it could be done once\n- Using `encoding/json` in ultra-hot paths where a faster serializer is warranted\n\n### Docker/gRPC Specific\n\n- Making redundant Docker API calls (e.g., inspecting a container multiple times)\n- Not using Docker API filters to narrow results server-side\n- Fetching all container logs when `tail` or `since` parameters should limit scope\n- gRPC streams not properly drained or closed\n- Creating new Docker/gRPC clients per request instead of reusing\n\n### General Go Anti-Patterns\n\n- `reflect` usage in hot paths\n- `fmt.Sprintf` for simple string operations where direct concatenation suffices\n- `interface{}` / `any` boxing causing heap escapes\n- Unnecessary use of `defer` in tight loops (small but real overhead)\n- `time.After` in select loops (creates new timer each iteration; use `time.NewTimer` + `Reset`)\n- Regex compilation inside functions instead of package-level `var` with `regexp.MustCompile`\n- Sorting large slices repeatedly instead of maintaining sorted order\n\n## Output Format\n\nUse ultra-brief mode as specified by the project:\n\n- Critical performance issues only\n- Brief bullet points with file:line references\n- Concrete suggestion for each issue\n- Maximum ~10-15 lines per response\n- No praise sections, no summaries, no fluff\n\nExample output:\n\n```\n- `internal/container/docker/client.go:142` — `io.ReadAll(resp.Body)` reads entire log stream into memory. Stream with `bufio.Scanner` instead.\n- `internal/web/logs.go:87` — New `json.Encoder` created per log line in hot loop. Reuse encoder or use `sync.Pool`.\n- `internal/hostservice/multi_host.go:53` — All hosts initialized eagerly at startup. Defer client creation until first access.\n```\n\nIf no performance issues are found, state that clearly in one line.\n\n**Update your agent memory** as you discover performance patterns, hot paths, allocation-heavy code paths, and architectural decisions in this codebase. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.\n\nExamples of what to record:\n\n- Hot paths identified (log streaming, stats collection, event processing)\n- Existing `sync.Pool` usage or buffer reuse patterns\n- Known allocation-heavy areas\n- Docker API call patterns and caching strategies\n- gRPC streaming patterns used in agent mode\n- Areas where lazy loading is already implemented vs. missing\n\n# Persistent Agent Memory\n\nYou have a persistent Persistent Agent Memory directory at `/Users/araminfar/Workspace/dozzle/.claude/agent-memory/go-perf-reviewer/`. Its contents persist across conversations.\n\nAs you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.\n\nGuidelines:\n\n- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise\n- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md\n- Update or remove memories that turn out to be wrong or outdated\n- Organize memory semantically by topic, not chronologically\n- Use the Write and Edit tools to update your memory files\n\nWhat to save:\n\n- Stable patterns and conventions confirmed across multiple interactions\n- Key architectural decisions, important file paths, and project structure\n- User preferences for workflow, tools, and communication style\n- Solutions to recurring problems and debugging insights\n\nWhat NOT to save:\n\n- Session-specific context (current task details, in-progress work, temporary state)\n- Information that might be incomplete — verify against project docs before writing\n- Anything that duplicates or contradicts existing CLAUDE.md instructions\n- Speculative or unverified conclusions from reading a single file\n\nExplicit user requests:\n\n- When the user asks you to remember something across sessions (e.g., \"always use bun\", \"never auto-commit\"), save it — no need to wait for multiple interactions\n- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files\n- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project\n\n## MEMORY.md\n\nYour MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.\n",".claude/skills/update-int-snapshots/SKILL.md":"---\nname: update-int-snapshots\ndescription: Use when Playwright visual snapshots in e2e/visual.spec.ts-snapshots/ need to be regenerated after a UI change (logo, theme, layout, anything that affects rendered pixels) and the integration suite needs to be rerun via make int\n---\n\n# Update Integration Snapshots\n\n## When to Use\n\n- A visual change landed (logo, color, spacing, fonts) and `make int` will fail on `visual.spec.ts` until snapshots are regenerated.\n- A snapshot test reports diffs you've confirmed are intentional.\n\nDo NOT use when diffs are unintentional regressions — investigate first.\n\n## Procedure\n\n1. **Delete the stale snapshots** so Playwright writes fresh ones (don't try to update in place, the old PNGs can confuse the diff):\n\n   ```bash\n   rm e2e/visual.spec.ts-snapshots/*.png\n   ```\n\n2. **Run with `--update-snapshots` via `compose up`.** You must use `compose up` (not `compose run`) because the navigation sidebar snapshot includes the live container list, and the two modes produce different sibling containers. The cleanest way is to patch the playwright command in `docker-compose.yml` temporarily:\n\n   ```bash\n   sed -i.bak 's|command: npx --yes playwright test|command: npx --yes playwright test --update-snapshots|' docker-compose.yml\n   make int\n   mv docker-compose.yml.bak docker-compose.yml\n   ```\n\n   Snapshots must be generated in Linux/Chromium (the compose image), not macOS, because filenames include the platform suffix (e.g. `-chromium-linux.png`).\n\n   Nothing needs clearing first. The compose file publishes no host ports, every container is named `dozzle_e2e_*` under the fixed project `dozzle_e2e`, and filters match those names exactly. Other containers on the machine can't block the run or show up in the snapshots, and a leftover run from another worktree is recreated. Two worktrees running `make int` at the same time do still clobber each other, so run one at a time.\n\n3. **Verify** by rerunning the normal suite:\n\n   ```bash\n   make int\n   ```\n\n   Should now pass cleanly.\n\n4. **Commit the regenerated PNGs** alongside the UI change so CI stays green.\n\n## Common Mistakes\n\n- **Running `npx playwright test --update-snapshots` locally on macOS.** Generates `-darwin` filenames CI doesn't use. Always go through the docker compose image.\n- **Forgetting to delete first.** `--update-snapshots` does overwrite, but if the test layout changed (new test, renamed snapshot), stale PNGs are left behind. Wipe-and-regenerate is safest.\n- **Adding a test container without the prefix, or a filter without anchors.** `name=dozzle` is a substring match and pulls in any local container with \"dozzle\" in its name. Use `container_name: dozzle_e2e_<service>` and `DOZZLE_FILTER=name=^dozzle_e2e_<service>$$` (`$$` escapes `$` in compose).\n- **Publishing a host port \"for debugging\".** It brings back conflicts with whatever else is on the machine. Reach instances by service name from inside the network.\n\n## Quick Reference\n\n```bash\nrm e2e/visual.spec.ts-snapshots/*.png\nsed -i.bak 's|command: npx --yes playwright test|& --update-snapshots|' docker-compose.yml\nmake int\nmv docker-compose.yml.bak docker-compose.yml\nmake int   # verify pass\n```\n",".claude/agents/bug-hunter.md":"---\nname: bug-hunter\ndescription: \"Use this agent when you need to review recently written or modified code for bugs, logic errors, edge cases, and unexpected behavior. This includes both Go backend code and Vue/TypeScript frontend code. This agent should be used after writing new features, fixing bugs, or refactoring code to catch issues before they reach production.\\\\n\\\\nExamples:\\\\n\\\\n- User writes a new HTTP handler in Go:\\\\n  user: \\\"I just added a new endpoint for container health checks\\\"\\\\n  assistant: \\\"Let me review the new code for potential bugs and edge cases.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to review the recently changed files>\\\\n\\\\n- User implements a new Vue composable:\\\\n  user: \\\"I created a new composable for managing WebSocket reconnection\\\"\\\\n  assistant: \\\"I'll launch the bug hunter to review your new composable for edge cases and potential issues.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to analyze the composable>\\\\n\\\\n- User modifies log parsing logic:\\\\n  user: \\\"I updated the event_generator.go to handle a new log format\\\"\\\\n  assistant: \\\"Let me have the bug hunter review your changes to ensure all edge cases in log parsing are covered.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to review the modified parsing code>\\\\n\\\\n- After a chunk of code is written during a feature implementation:\\\\n  assistant: \\\"I've finished implementing the notification dispatcher. Let me run the bug hunter to check for issues.\\\"\\\\n  <uses Task tool to launch bug-hunter agent proactively to review the new code>\"\nmodel: opus\ncolor: blue\nmemory: project\n---\n\nYou are an elite bug-hunting code reviewer with deep expertise in both Go backend systems and Vue 3/TypeScript frontend applications. You have extensive experience finding subtle bugs, race conditions, nil pointer dereferences, unhandled edge cases, type safety issues, and logic errors that slip past typical review. You think adversarially — always asking \"what could go wrong here?\"\n\n## Your Mission\n\nReview recently written or modified code to find bugs, logic errors, and uncovered edge cases. You focus on code that could fail at runtime, produce incorrect results, or behave unexpectedly under specific conditions.\n\n## Review Process\n\n1. **Identify Changed/New Code**: Use git status, git diff, or examine the files the user points you to. Focus on recently written code, not the entire codebase.\n\n2. **Analyze Each File Systematically**: For every file with changes, examine:\n   - Control flow paths (all branches of if/else, switch, select)\n   - Error handling (are errors checked? propagated correctly? logged?)\n   - Nil/undefined checks (pointer dereference in Go, optional chaining in TS)\n   - Concurrency safety (goroutine leaks, race conditions, channel operations)\n   - Resource cleanup (deferred closes, stream cleanup, event listener removal)\n   - Boundary conditions (empty arrays, zero values, max values, negative numbers)\n   - Type safety (type assertions in Go, type narrowing in TS)\n   - API contract adherence (correct HTTP status codes, proper SSE format, GraphQL schema alignment)\n\n3. **Cross-Layer Analysis**: Check that frontend and backend changes are consistent:\n   - API request/response shapes match between Go handlers and TypeScript types\n   - SSE event names and payload structures align\n   - GraphQL schema, resolvers, and frontend queries are in sync\n   - WebSocket message formats match on both sides\n\n## Go-Specific Bug Patterns to Check\n\n- **Nil pointer dereference**: Especially after type assertions, map lookups, and interface conversions\n- **Goroutine leaks**: Goroutines blocked on channels that are never closed or written to\n- **Race conditions**: Shared state accessed from multiple goroutines without synchronization\n- **Deferred closure in loops**: `defer` inside loops won't execute until function returns\n- **Error shadowing**: Using `:=` that shadows an outer `err` variable\n- **Slice/map initialization**: Operating on nil slices/maps (nil map write panics)\n- **Context cancellation**: Not respecting context.Done() in long-running operations\n- **HTTP response body leaks**: Not closing response bodies after HTTP calls\n- **Integer overflow**: Especially in stats calculations with uint64\n- **String/byte slice sharing**: Modifying a slice that shares underlying array\n- **Channel operations on nil channels**: Blocking forever on nil channel send/receive\n- **Mutex copy**: Passing sync.Mutex by value instead of pointer\n\n## Vue/TypeScript-Specific Bug Patterns to Check\n\n- **Reactive reference unwrapping**: Using `.value` correctly with `ref()` vs `reactive()`\n- **Computed dependency tracking**: Missing reactive dependencies in computed properties\n- **Watch cleanup**: Not cleaning up watchers, event listeners, or timers in `onUnmounted`\n- **SSE/EventSource cleanup**: Connections not properly closed on component unmount\n- **Array reactivity**: Using index assignment instead of reactive methods\n- **Optional chaining gaps**: Accessing nested properties without null checks\n- **Promise error handling**: Unhandled promise rejections, missing `.catch()` or try/catch\n- **Type narrowing issues**: Assuming a type without proper guards\n- **Stale closure references**: Callbacks capturing outdated reactive values\n- **Memory leaks**: Growing arrays without bounds (check maxLogs enforcement, statsHistory rolling window)\n- **Race conditions in async operations**: Component unmounted before async operation completes\n- **Incorrect v-if/v-show usage**: Rendering components that depend on data not yet loaded\n- **Event buffer overflow**: Not handling backpressure in SSE streams\n\n## Project-Specific Concerns\n\n- **EMA calculations**: Alpha=0.2 for stats smoothing — verify division by zero, NaN handling\n- **Rolling window (300 items)**: Ensure proper eviction and no off-by-one errors\n- **Log entry type discrimination**: `LogEntry.create()` must handle all `logEvent.t` values\n- **Multi-host operations**: Container/host lookups via `FindContainer()`/`FindHost()` may return nil\n- **Docker API version negotiation**: Calls may fail on older Docker versions\n- **Protobuf serialization**: `FromProto()` methods must handle nil/empty fields\n- **Authentication modes**: Code must work correctly in all three auth modes (none, simple, forward-proxy)\n- **SSE buffering**: 250ms debounce with 1000ms max — verify timer cleanup\n- **CPU normalization**: Division by `cpuLimit` or `nCPU` — check for zero values\n\n## Output Format\n\nFollow ultra-brief mode as specified in the project guidelines:\n\n- Critical issues only (bugs, security, blockers)\n- Brief bullet points, no lengthy explanations\n- Skip verbose sections (no \"Strengths\", \"Summary\", etc.)\n- Include file:line references when relevant\n- Maximum ~10-15 lines per response\n\nFor each bug found, report:\n\n- **File:line** — Brief description of the bug\n- **Severity**: 🔴 Critical (will crash/corrupt) | 🟡 Warning (could fail under specific conditions) | 🟠 Edge case (uncovered scenario)\n- One-line fix suggestion if obvious\n\nIf no bugs are found, say so concisely. Do not fabricate issues.\n\n## Approach\n\n1. First, determine what code was recently changed (check git diff or ask the user)\n2. Read the changed files carefully\n3. For each file, apply the relevant bug pattern checklist above\n4. Cross-reference frontend and backend changes for consistency\n5. Report findings in the ultra-brief format\n\nDo NOT review code style, naming conventions, or suggest refactors unless they mask a bug. Focus exclusively on correctness.\n\n**Update your agent memory** as you discover recurring bug patterns, common error-prone code paths, areas of the codebase with historical issues, and edge cases specific to this project's architecture. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.\n\nExamples of what to record:\n\n- Recurring nil-check omissions in specific packages\n- Components that frequently have cleanup issues\n- API endpoints with known edge case gaps\n- Stats calculation patterns that are error-prone\n- Log parsing paths that have caused issues before\n\n# Persistent Agent Memory\n\nYou have a persistent Persistent Agent Memory directory at `/Users/araminfar/Workspace/dozzle/.claude/agent-memory/bug-hunter/`. Its contents persist across conversations.\n\nAs you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.\n\nGuidelines:\n\n- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise\n- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md\n- Update or remove memories that turn out to be wrong or outdated\n- Organize memory semantically by topic, not chronologically\n- Use the Write and Edit tools to update your memory files\n\nWhat to save:\n\n- Stable patterns and conventions confirmed across multiple interactions\n- Key architectural decisions, important file paths, and project structure\n- User preferences for workflow, tools, and communication style\n- Solutions to recurring problems and debugging insights\n\nWhat NOT to save:\n\n- Session-specific context (current task details, in-progress work, temporary state)\n- Information that might be incomplete — verify against project docs before writing\n- Anything that duplicates or contradicts existing CLAUDE.md instructions\n- Speculative or unverified conclusions from reading a single file\n\nExplicit user requests:\n\n- When the user asks you to remember something across sessions (e.g., \"always use bun\", \"never auto-commit\"), save it — no need to wait for multiple interactions\n- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files\n- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project\n\n## MEMORY.md\n\nYour MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.\n","CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Response Style\n\nKeep chat responses brief: bullet points, `file:line` references, no \"Strengths\" or\n\"Summary\" sections. What the PR review bot looks for is set in\n`.github/workflows/claude-code-review.yml`, not here.\n\n## Translations\n\n`locales/en.yml` is the source of truth, but it is never the only file to change. Whenever you add, rename, or remove a key in `en.yml`, apply the same change to **every** other file in `locales/` in the same commit, with the string actually translated (not the English text copied over). Keys are also deleted everywhere when they lose their last consumer.\n\nThere is no Crowdin or Weblate sync in this repo, so a key that only exists in `en.yml` silently falls back to English for every other locale and nothing ever flags it.\n\n### Docs Translations\n\n`docs/` is translated into `zh`, `de`, `fr` and `es`. The English page under `docs/` is the source of truth, and `docs/<locale>/<same path>` mirrors it. Changing an English page means updating all four translations in the same commit.\n\nUnlike `locales/`, there is no fallback here. A stale translated page renders confidently wrong instructions rather than quietly showing English, so drift is worse than a missing key.\n\nEach translated file carries a `sourceHash` in its frontmatter recording the English source it was written from. `bun docs/scripts/check-translations.mjs` fails when they diverge, and runs in CI as the `Docs Translations` job. After actually translating the changed prose, re-stamp with:\n\n```bash\nbun docs/scripts/check-translations.mjs --update\n```\n\n`--update` only re-stamps hashes. It does not translate anything, so running it on an untranslated page turns CI green while leaving the page wrong. Translate first.\n\nInside translated pages, everything in a fenced code block stays byte-identical to English except natural-language comments, and internal links are locale-prefixed (`/guide/agent` becomes `/de/guide/agent`). The sidebar is not duplicated per locale: its structure lives in `docs/.vitepress/locales/structure.ts` and each locale file supplies only labels, so a new page needs one slug there plus one label per locale.\n\n## Testing Unreleased PRs\n\nWhen replying to a GitHub issue or discussion where the fix lives in an open PR, ask the reporter to test the pre-built image: `amir20/dozzle:pr-XXX` (XXX = PR number). CI builds a tagged image per PR, so reporters can verify without waiting for the next release.\n\n## GitHub Tone (issues, PRs, comments, discussions)\n\nWhen posting anything to GitHub, write like a human maintainer, not an AI assistant. Avoid telltale LLM patterns:\n\n- No em dashes or en dashes. Use commas, periods, or parentheses instead.\n- No \"Not X, but Y\" rhetorical contrasts.\n- No throat-clearing openers (\"Great point\", \"Makes sense\", \"Thanks for the detailed write-up\").\n- No closing summaries or recap sentences.\n- No bolded inline labels mid-paragraph (\"**Why:**\", \"**Note:**\").\n- Drop hedges (\"essentially\", \"basically\", \"essentially just\"). Say it plain.\n- Lowercase casual tone is fine. Contractions are fine. Short sentences are fine.\n- Don't over-explain tradeoffs. State the decision, give one reason, stop.\n\n## Scope Discipline\n\nSmall first versions win. Building is cheap here; reviewing and maintaining is not.\n\n- **Agree the scope as a changelog before any code.** For a new feature, write what\n  ships (user-visible bullets) and an explicit \"Not in this release\" list, and get it\n  approved. Anything not on the ship list is out of scope, however reasonable it looks.\n- **A plan is a menu, not a work order.** Build phase 1 only. Later phases start after\n  phase 1 has merged, unless the user asks for more.\n- **Reuse before you add.** A new table, RPC, proto field, flag, label or setting needs a\n  one-line reason why an existing one (labels, existing events, existing alert channels,\n  existing pages) can't do it. Prefer one delivery surface to several.\n- **Stop at ~1,000 lines.** If a PR's non-test, non-generated diff passes ~1,000 lines,\n  stop and check scope with the user before continuing.\n- **One feature, one PR per repo.** Unrelated work (e.g. a settings redesign during an\n  updates feature) gets its own PR off main, never a layer in the same stack. Avoid\n  stacks deeper than two; with a GitHub stack, merge from the bottom.\n- **Subagents and workflows get the out-of-scope list.** Every implementation prompt\n  includes \"not in scope\" and the rule: a deviation that adds behaviour is reported,\n  not built.\n- **Review fixes fix; they don't extend.** Addressing feedback must not add new\n  mechanisms. If a fix needs one, raise it as a scope question.\n- **Audit size before review, not after.** When a body of work is done, report its size\n  per feature (code, tests, generated) next to its user impact, so cuts happen before\n  review time is spent.\n\n## Project Overview\n\nDozzle is a lightweight, web-based Docker log viewer with real-time monitoring capabilities. It's a hybrid application with:\n\n- **Backend**: Go (HTTP server, Docker API client, WebSocket streaming)\n- **Frontend**: Vue 3 (SPA with Vite, TypeScript)\n\nThe application supports multiple deployment modes: standalone server, Docker Swarm, and Kubernetes (k8s).\n\n## Development Commands\n\n### Setup\n\n```bash\n# Install dependencies\nbun install\n\n# Generate certificates and protobuf files\nmake generate\n```\n\n### Development\n\n```bash\n# Run full development environment (backend + frontend with hot reload)\nmake dev\n\n# Same, on a free port trio derived from this checkout's path, so several worktrees\n# can each run an instance at once. Prints the URL it picked.\nmake dev-auto\n\n# Alternative: Run backend and frontend separately\nbun run watch:backend  # Go backend with air (port 3100)\nbun run watch:frontend # Vite dev server (port 3100)\n\n# Run in agent mode for development\nbun run agent:dev\n```\n\n### Building\n\n```bash\n# Build frontend assets\nbun run build\n# or\nmake dist\n\n# Build entire application (includes frontend build)\nmake build\n\n# Build Docker image\nmake docker\n```\n\n### Testing\n\n```bash\n# Run Go tests\nmake test\n\n# Run frontend tests (Vitest)\nbun run test\n# Run in watch mode\nTZ=UTC bun run test --watch\n\n# Type checking\nbun run typecheck\n\n# Lint (Tailwind classes only)\nbun run lint\nbun run lint --fix\n```\n\nAlways go through `bun run` for `test` and `build`. `bun test` and `bun build` are\nbun's own test runner and bundler, not the scripts in `package.json`, and they fail\nin confusing ways against this codebase.\n\n`bun run` does not mean the bun runtime: it follows each tool's `#!/usr/bin/env node`\nshebang unless the script says `bun --bun`. Every script in `package.json` that starts a\nJS tool carries that flag except `test` and `typecheck`, which need node: under `--bun` vitest's jsdom\nworkers fail to start and `vue-tsc` reports a false TS2614. `release` (bumpp) is\nuntested on bun and also left alone. Those are the only reasons node is still required.\n\n### Linting\n\nESLint here checks Tailwind classes and nothing else. It is not a general JS or TS\nlinter, and it should not become one: `tsconfig.json` already has `strict`,\n`strictNullChecks` and `noUnusedLocals`, so `vue-tsc` covers that ground, and prettier\nplus `prettier-plugin-tailwindcss` own formatting and class order. No stylistic rule\nbelongs in `eslint.config.js`, which is also why `eslint-config-prettier` is not needed.\n\nEvery rule the Tailwind plugin ships is listed in that config with the reason it is on\nor off, so add to that list rather than reaching for a preset.\n\n`eslint --fix` runs on staged `js`/`mjs`/`ts`/`mts`/`vue` in the pre-commit hook, before\nprettier so prettier gets the last word on formatting. Run `bun run lint --fix` by hand\nafter writing a batch of markup if you would rather not find out at commit time.\n\nTwo blind spots worth knowing, because a green run does not mean a clean tree:\n\n- `@apply` inside an SFC `<style>` block is never handed to the plugin, so a bad class\n  there is never reported.\n- The arbitrary-value rule only maps whole-number spacing steps. `gap-[2px]` and `w-[3px]`\n  go unreported even though `gap-0.5` and `w-0.75` exist.\n\n### Preview & Other\n\n```bash\n# Preview production build locally\nbun run preview\n# or\nmake preview\n\n# Run integration tests (Playwright)\nmake int\n```\n\n## Architecture\n\n### Backend (Go)\n\nThe Go backend is organized into these key packages:\n\n- **`internal/web/`** - HTTP server and routing layer\n  - Routes defined in `routes.go` using chi router\n  - WebSocket/SSE handlers for log streaming (`logs.go`)\n  - Authentication middleware and token management (`auth.go`)\n  - Container action handlers (`actions.go`)\n  - `sse/`: the SSE writer; `search/`: log filtering, highlighting and HTML escaping\n\n- **`internal/container/`** - The generic layer every platform implements\n  - `types.go`, `host.go`: domain models (`Container`, `Host`, `LogEvent`, `ContainerStat`)\n  - `client.go`: `container.Client`, the raw engine interface\n  - `client_service.go`: `container.ClientService`, the per-host contract the rest of the app uses\n  - `store.go`: the container cache docker and k8s share (its header comment explains who\n    may write to the map), split by concern into `store_events.go` (the event loop),\n    `store_refresh.go` (reconnect, list and reconcile) and `store_fanout.go`\n    (bounded delivery to subscribers)\n  - `collector_lifecycle.go`: the lazy start/idle-stop reference count both stats collectors use\n  - `logparse/`: the log pipeline both platforms feed (`event_generator.go` grouping and JSON\n    detection, `level_guesser.go`, `logfmt.go`, `timestamp_prefix.go`)\n  - One folder per platform, each holding that platform end to end:\n    - `docker/`: `client.go`, `log_reader.go`, `stats_collector.go`, and `service.go` (`docker.Service`)\n    - `k8s/`: `client.go`, `log_reader.go`, `stats_collector.go`, and `service.go` (`k8s.Service`)\n    - `agent/`: gRPC `client.go`/`server.go`, `convert.go` (proto conversion), `service.go`, and\n      generated `pb/` (protos in `protos/`)\n\n- **`internal/hostservice/`** - Orchestration across hosts, one per deployment mode\n  - `multi_host.go`: `MultiHostService` (server and swarm modes)\n  - `retriable_client_manager.go` (server), `swarm_client_manager.go` (swarm)\n  - `k8s_cluster.go`: `K8sClusterService` (k8s mode)\n\n- **`internal/cli/`** - Command-line parsing, subcommands, client wiring (`clients.go`)\n\n- **`internal/cloud/`** - Dozzle Cloud integration (tool execution engine)\n  - `client.go`: Bidirectional gRPC stream client with auto-reconnect and exponential backoff\n  - `tools.go`: Tool registration, dispatch (`executeTool`), and `ToolHostService` interface\n  - `tools_containers.go`: Container listing, finding, stats, and inspection tools\n  - `tools_logs.go`: Log fetching with level/query/regex filtering (max 100 lines)\n  - `tools_actions.go`: Container start/stop/restart actions (gated by `enableActions`)\n  - `tools_helpers.go`: Proto conversion utilities and host name resolution\n  - Uses `protos/cloud.proto` for service and message definitions\n\n- **`internal/auth/`** - Authentication providers\n  - Simple file-based auth (`simple.go`)\n  - Forward proxy auth (`proxy.go`)\n  - Role-based authorization (`roles.go`)\n\n- **`internal/notification/`** - Alert and notification system\n  - `manager.go`: Notification rule evaluation and dispatching\n  - `log_listener.go`: Log pattern matching for alerts\n  - `dispatcher/`: Notification channel implementations (email, webhook, etc.)\n\n- **`main.go`** - Application entry point with mode switching (server/swarm/k8s/agent)\n\n### Frontend (Vue 3)\n\nThe frontend uses file-based routing with these conventions:\n\n- **`assets/pages/`** - File-based routes (unplugin-vue-router)\n  - `container/[id].vue`: Single container view\n  - `merged/[ids].vue`: Multi-container merged view\n  - `host/[id].vue`: Host-level logs\n  - `service/[name].vue`: Swarm service logs\n  - `stack/[name].vue`: Docker stack logs\n  - `group/[name].vue`: Custom grouped logs\n\n- **`assets/components/`** - Vue components (auto-imported). See \"Where files go\" below.\n  - `ui/`: generic primitives (`BarChart.vue`, `MetricCard.vue`, `Popover.vue`, `UsageMeter.vue`, ...)\n  - `shell/`: app frame and the singletons `layouts/default.vue` mounts once\n  - `nav/`: everything inside the sidebar\n  - `search/`: the command palette and search surfaces\n  - `containers/`: `ContainerTable.vue`, the toolbar, the terminal\n  - `hosts/`: `HostCard.vue` and friends\n  - `logs/`: the stream, with `logs/entries/` holding one file per `LogEntry` type\n    (`SimpleLogItem.vue`, `ComplexLogItem.vue`, `GroupedLogItem.vue`,\n    `ContainerEventLogItem.vue`, `SkippedEntriesLogItem.vue`, `LoadMoreLogItem.vue`)\n  - `views/`: one file per route that renders a log view, mounted by `assets/pages/`\n  - `notifications/`: the rules the user wrote\n  - `cloud/`: memory, cross-instance and account, split into `rail/`, `chat/`, `history/`\n\n- **`assets/stores/`** - Pinia stores (auto-imported)\n  - `config.ts`: App configuration and feature flags (injected from backend HTML, frozen immutable)\n  - `container.ts`: Container state management with EventSource streaming (`/api/events/stream`)\n  - `hosts.ts`: Multi-host state\n  - `settings.ts`: User preferences (localStorage-backed via profileStorage)\n  - `pinned.ts`: Pinned container logs for side-by-side viewing\n  - `swarm.ts`, `k8s.ts`: Deployment mode-specific state\n  - `announcements.ts`: Feature announcements\n\n- **`assets/composable/`** - Vue composables (auto-imported), foldered to mirror the components\n  - `ui/`: `popover.ts`, `media.ts`, `timeTicker.ts` - no domain knowledge\n  - `app/`: shell and global session state (`drawer.ts`, `theme.ts`, `toast.ts`, ...)\n  - `logs/`: the stream pipeline (`eventStreams.ts` SSE with 250ms buffered flushing,\n    `historicalLogs.ts`, `logContext.ts` provide/inject, `scrollContext.ts`,\n    `visible.ts`, `duckdb.ts` for SQL over logs)\n  - `containers/`: `containerActions.ts`, `imageUpdate.ts`, ...\n  - `cloud/`, `notifications/`, `editor/`\n\n  The `AutoImport` `dirs` glob is `assets/composable/**`. It must stay recursive: a bare\n  directory is scanned one level deep, and nested composables silently stop being\n  auto-imported with no error. The `**` also globs `*.spec.ts`, which is harmless only\n  while no spec exports anything.\n\n- **`assets/modules/`** - Vue plugins\n  - `router.ts`: Vue Router configuration\n  - `pinia.ts`: Pinia store setup\n  - `i18n.ts`: Internationalization\n\n### Communication Flow\n\n1. **Real-time Logs**: Frontend establishes SSE connections to `/api/containers/logs/stream?container={host}/{id}` (repeated for a merged view)\n2. **Container Events**: SSE stream at `/api/events/stream` pushes container lifecycle events\n3. **Stats**: Real-time CPU/memory stats streamed via SSE alongside events\n4. **Actions**: POST to `/api/hosts/{host}/containers/{id}/actions/{action}` (start/stop/restart)\n5. **Terminal**: WebSocket connections for container attach/exec at `/api/hosts/{host}/containers/{id}/attach`\n\n### Build System\n\n- **Frontend**: Vite builds to `dist/` with manifest\n- **Backend**: Embeds `dist/` using Go embed directive\n- **Hot Reload**: In development, `DEV=true` disables embedded assets, `LIVE_FS=true` serves from filesystem\n- **Makefile**: Orchestrates builds and dependency generation\n\n## Dozzle and Dozzle Cloud\n\nCloud features are folded into the Dozzle UI rather than linked to. The split that\ndecides where a feature lands:\n\n> **Dozzle owns anything about the container in front of the user. Cloud owns\n> anything that is history, cross-instance, or account.**\n\nDozzle is optimized for the current view and can act on it; Cloud reviews everything\nat once. When a feature could live in either, ask which of those two questions it\nanswers.\n\n### Naming\n\nThe two Cloud surfaces must not blur into each other:\n\n- **Notifications** — the rules the user wrote, and what those rules did.\n- **Findings** — the rules the user never wrote, that Cloud noticed anyway.\n\nFindings are not alerts and are never listed alongside them.\n\n### What Cloud adds is memory, not features\n\nNothing Dozzle does locally today ever gets gated. Alerts already splice into the log\nstream (`AlertLogItem.vue`), they just die on refresh because nothing remembers them.\nCloud's contribution is that the same thing survives a reload, a restart, and a week —\nwhich is why an alert can come back later as a dot on a container row.\n\nSo a Cloud-less install shows an empty history section with one muted line saying what\nit would hold, not a locked card. A surface that has no local half at all (a findings\nlist) is mounted only when cloud is configured, rather than existing as a permanent\nupsell page in the nav.\n\n### Linking out\n\nLinking out to Cloud is fine, and expected, for anything Cloud genuinely does better:\nbilling and keys, the report archive, cross-instance rollups, deep-investigation\ntranscripts. `AlertHit.url` in `protos/cloud.proto` already carries the deep link.\n\nNever link out for something Dozzle could answer against the stream already on screen.\n\"Show me the lines\" — driving the historical scroll to the window a finding or alert\ndescribes — is the whole reason these surfaces live in Dozzle at all.\n\n### Gating\n\nPlan decisions are computed server-side in Cloud and handed to Dozzle already decided\n(`fixLocked`), never inferred from missing data — absence read as tier is how a paying\ncustomer gets shown an upsell for what they already bought. Dozzle renders the gate it\nis given and branches on nothing beyond `isPro` for cosmetics.\n\nOne upsell surface: `CloudPopover`. Everywhere else stays silent, per the design\nsystem's rule that `primary` is reserved for the single action a surface wants you to\ntake — a findings drawer wants \"Show me the lines\", not \"Upgrade\".\n\n## Design System\n\nThe UI is being converged on one visual language: clean, flat, and quiet. New or\nredesigned surfaces follow these rules; when touching an old surface, bring it along\nrather than matching what is already there.\n\n### Surfaces\n\n- A grouped surface is a **neutral panel**: `rounded-lg border border-base-content/15 bg-base-200/40`,\n  with rows separated by `divide-y divide-base-content/10`. Padding is `p-4` per row,\n  `p-2` for a row of link items. No shadows, no gradients, no `--depth`.\n- Floating surfaces (dropdowns, popovers, toasts) use `rounded-box border border-base-content/10 bg-base-200`\n  with `shadow-sm`/`shadow-lg`. `nav-menu-panel` in `main.css` is the sidebar's version.\n- Inside a panel, a hairline `<div class=\"bg-base-content/10 h-px\">` separates blocks when\n  `divide-y` does not apply (e.g. inside a dropdown's flow).\n- Interactive rows are `hover:bg-base-300 rounded-md px-2 py-1.5 transition-colors`, with a\n  leading icon at `size-4 opacity-60` and a trailing `mdi:open-in-new size-3.5 opacity-40`\n  when the row leaves the app.\n\n### Color and severity\n\n- **Severity rides on the icon, never on the surface.** A tinted circle\n  (`bg-error/10 text-error`, `bg-warning/10 text-warning`, `bg-info/10 text-info`) plus a\n  status dot carries the state; the panel behind it stays neutral so the text keeps full\n  contrast. Do not use daisyUI `alert alert-error` / `alert-warning` / `alert-success`:\n  a saturated block at drawer or page width shouts over everything near it.\n- Use `InlineNotice` (`assets/components/ui/InlineNotice.vue`) for an in-page notice and\n  `ToastModal` for a floating one. Both are neutral panels with a tinted glyph.\n- Status text is a `status-pill` (see `main.css`): a bordered, uppercase, mono chip in\n  `neutral`, `success`, `primary`, `secondary`, `warning` or `error`. Prefer it to `badge`.\n- `primary` is reserved for the single action a surface wants you to take. Two solid\n  primary buttons in one panel is a bug; the secondary one is a plain `btn`, and a\n  destructive secondary is `btn text-error`, not `btn-error`.\n\n### Typography and density\n\n- Body `text-sm`, secondary copy `text-base-content/60`, footnotes `text-xs` at\n  `text-base-content/40`. Drawer and page titles are `text-2xl font-bold` with a\n  `text-base-content/60` subtitle underneath.\n- Section headings inside a form are `FormStepHeading` (numbered chip + uppercase label);\n  standalone section headings are `text-base-content/60 font-semibold tracking-wide uppercase`.\n- Numbers, keys, IDs, periods, and percentages are `font-mono`. Emphasize the meaningful\n  half and mute the rest (`font-semibold` used, `text-base-content/40` for `/ limit`).\n- A read-only value is text, not a disabled input. Label left, mono value right.\n\n### Meters and charts\n\n- Usage meters are `UsageMeter` (`assets/components/ui/UsageMeter.vue`): a\n  `bg-base-content/10 h-1.5 rounded-full` track with a `bg-primary` fill that turns\n  `bg-warning` past 70% and `bg-error` past 90%. Do not use daisyUI `<progress>` for these;\n  it is taller than the type around it and carries its own palette.\n- Charts stay custom and lightweight (`BarChart.vue`); no chart library.\n\n### Affordances\n\n- Tailwind v4's preflight sets `cursor: default` on buttons, so a control reads as\n  painted on unless something puts the pointer back. One base-layer rule in `main.css`\n  does that for `button`, `summary`, `select`, `[role=\"button\"]`, `.btn` and\n  checkbox/radio labels, skipping disabled ones. Never add `cursor-pointer` to a new\n  button; if a clickable thing still shows an arrow, it is a `div` pretending to be a\n  button and the fix is to make it a `<button>`.\n- Anything clickable needs a hover state and a hit area a pointer can find. Under ~24px\n  the painted box is not the target: draw the hit area with an `::after` inset so\n  the layout around it does not move (`AlertDot.vue`).\n\n### Motion\n\n- Transitions are `transition-colors` on hover, `duration-500` on a meter's width, and\n  `200ms cubic-bezier(0.34, 1.56, 0.64, 1)` for the icon spring in `main.css`.\n- Icon-only buttons opt into the spring with `btn-circle`, `btn-square` or `icon-btn`; the\n  flavours (`icon-float`, `icon-spin`, `icon-wiggle`, `icon-ring`) are set on the glyph.\n- Every animation has a `prefers-reduced-motion` escape.\n\n### Mobile\n\n- Under `isMobile` the shell is a floating tab bar (`shell/MobileTabBar.vue`: Home, Browse,\n  Notifications, Settings, plus a separate search button). A log route is a pushed screen:\n  no tab bar, and `ScrollableView`'s header carries a back button. Which routes count as log\n  routes is `LOG_ROUTES` in `composable/app/mobileShell.ts`; a new log view goes there.\n- Floating chrome on a phone uses the `frosted` utility (translucent fill + backdrop blur),\n  which falls back to a solid `base-200` without `backdrop-filter` or under\n  `prefers-reduced-transparency`. It is a fill only; add the border and shadow yourself. Do\n  not use daisyUI's `glass`, which paints a diagonal sheen. Panels and rows stay flat.\n- A menu that is an action list opens as a bottom sheet on a phone: pass `sheet` to `Popover`.\n\n### Consistency rules\n\n- A panel keeps its shape across states. Loading, error and healthy branches of the same\n  surface share the header, dividers and footer so nothing reshuffles when state changes.\n- The same concept looks the same everywhere. The cloud account panel is the same parts at\n  three widths: `CloudPopover.vue` (compact), `CloudSettingsCard.vue`, and\n  `notifications/CloudDestinationForm.vue`. When a fourth surface needs it, extract a\n  component instead of copying the classes.\n- Do not repeat the container's title inside its own content: a drawer header already names\n  the thing, so the panel below leads with what the header cannot say.\n- Drawer footers are sticky, opaque and full-bleed, bled over the drawer's `p-6` and padded\n  clear of the home indicator:\n  `bg-base-100 border-base-content/10 sticky -bottom-6 z-10 -mx-6 -mb-6 mt-auto border-t px-10 pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]`\n  (widen `-mx`/`-mb` by any padding the occupant adds of its own).\n\n## Where files go\n\nFour tests for a new component, applied in order. The first that matches decides the\nfolder, so a contributor never has to ask.\n\n1. **`ui/`** — it imports nothing under `@/` except `@/composable/ui/`, **and** its name\n   carries no Dozzle noun (container, host, log, alert, cloud, nav, swarm, k8s, stack,\n   service, chat, rail). `ui/` means \"would still build pasted into a different Vue app\".\n   It does not mean \"shared\" and it does not mean \"small\": a `ui/` component with one\n   consumer is fine, and a large domain component with six consumers still lives in its\n   feature folder. Genericity is the test, popularity is not.\n2. **`cloud/`** — the surface does not exist at all in a cloud-less install. Two carve-outs,\n   settled once: anything `assets/models/LogEntry.ts` registers is a log entry and stays in\n   `logs/entries/` with its siblings, `AlertLogItem.vue` included, because alerts splice\n   into the local stream. And a variant that plugs into a local form stays with that form,\n   which is why `CloudDestinationForm.vue` sits beside `WebhookDestinationForm.vue`.\n3. **`views/`** — a file in `assets/pages/` mounts it as that route's whole body. One file\n   per route. (`ContainerLog.vue` is also mounted by `layouts/default.vue` for pinned\n   columns; it is still that route's view.)\n4. Otherwise the **feature folder** named by the noun in the component's own name.\n\n`nav/` is everything inside the sidebar; `shell/` is the frame that positions it. Nest one\nlevel inside a feature folder only past ~15 files and only with an obvious sub-noun, never\ntwo. A spec moves with its subject. `assets/composable/` uses the same folder names and the\nsame first two tests.\n\n`components/ui/` may depend on `composable/ui/` and third-party code, and nothing else.\nThat is the one dependency edge worth enforcing in review.\n\nMoving a component is cheap: `unplugin-vue-components` runs without `directoryAsNamespace`,\nso a component's name is its filename and nesting is invisible to every template. It also\nmeans **two components may never share a basename**, at any depth.\n\n## Important Development Notes\n\n### Frontend\n\n- Auto-imports are configured for Vue composables, components, and Pinia stores (see `vite.config.ts`)\n- Icons use unplugin-icons with multiple icon sets (mdi, carbon, material-symbols, etc.)\n- Tailwind CSS with DaisyUI for styling\n- TypeScript definitions auto-generated in `assets/auto-imports.d.ts` and `assets/components.d.ts`\n- **Log Entry Types**: Three types of log messages supported\n  - `SimpleLogEntry`: Single-line text logs (`string`)\n  - `ComplexLogEntry`: Structured JSON logs (`JSONObject`)\n  - `GroupedLogEntry`: Multi-line grouped logs (`string[]`)\n- **Type consistency**: Use `LogMessage` type alias instead of `string | string[] | JSONObject` for log entry messages\n- **Log Entry Factory Pattern**: Use `LogEntry.create(logEvent)` to instantiate the correct entry type based on `logEvent.t` field\n- **EventSource Buffering**: Log streams use buffer-based flushing (250ms debounce, 1000ms max) to batch UI updates\n- **Charts/Visualizations**: Custom lightweight implementations (no D3.js)\n  - `BarChart.vue`: Self-contained bar chart with responsive downsampling\n  - Downsampling algorithm: Averages data into buckets based on available screen width\n  - All stat history tracked in `Container.statsHistory` (max 300 items via rolling window)\n  - `chartData` is always a rolling window of max 300 items — array length stays constant\n  - Uses `ref` (not `computed`) for `downsampledBars` to enable in-place mutation of the last bar, avoiding full re-renders\n  - Component instance is reused when switching containers; after init the chart only patches the last bar per tick, so on a wholesale `chartData` replacement (container switch) the parent must call the exposed `recalculate()`. `MultiContainerStat` holds refs to its `BarChart`s and calls it in the `containers` watch. (Note: `Container` carries Vue `ref`s, so VueTestUtils `setProps` cannot retrigger such a watch — tests must swap the container via a parent `ref` re-render.)\n\n### Backend\n\n- The application uses Go 1.25+ with module support\n- Certificate generation is required (`make generate` creates shared_key.pem and shared_cert.pem)\n- Protocol buffer generation happens via `go generate` directive in `main.go`\n- Docker client uses API version negotiation for compatibility\n- **Service Layer Architecture**:\n  - `container.ClientService` interface abstracts Docker/K8s/Agent backends\n  - `hostservice.MultiHostService` orchestrates multi-host operations\n  - `ClientManager` implementations: `RetriableClientManager` (server mode), `SwarmClientManager` (swarm mode)\n- **Where backend code goes**: code that only one platform needs lives in `internal/container/<platform>/`;\n  code docker and k8s share lives in `internal/container/` (or `logparse/` for the log pipeline);\n  code that coordinates several hosts lives in `internal/hostservice/`. `internal/container` never\n  imports a platform package, and platforms never import each other.\n\n### Authentication\n\n- Three modes: none, simple (file-based users.yml), forward-proxy (e.g., Authelia)\n- JWT tokens for simple auth with configurable TTL\n- User file location: `./data/users.yml` or `./data/users.yaml`\n\n### Testing\n\n- Go tests use standard `testing` package with testify assertions\n- Frontend uses Vitest with `@vue/test-utils`\n- Integration tests with Playwright in `e2e/`\n- Tests must run with `TZ=UTC` for consistent timestamps\n\n### Container Stats & Metrics\n\n- Stats are tracked using exponential moving average (EMA) with alpha=0.2\n- History stored in rolling window (300 items max) via `useSimpleRefHistory`\n- CPU metrics normalized by core count (respects `cpuLimit` or falls back to host `nCPU`)\n- Memory metrics include both percentage and absolute usage (`memoryUsage` vs `memory`)\n- Stats visualization uses adaptive downsampling for performance\n\n### Container Labels\n\n- `dev.dozzle.name`: Custom container display name\n- `dev.dozzle.group`: Group containers together\n- `dev.dozzle.url`: Link a container to its own web UI (http/https only; `Container.url` in `assets/models/Container.ts`)\n- Label-based filtering throughout the application\n\n### Deployment Modes\n\n- **Server mode** (default): Single or multi-host Docker monitoring\n  - Uses `RetriableClientManager` with local + remote agent clients\n- **Swarm mode**: Automatic discovery of Swarm nodes via Docker API\n  - Creates gRPC agent server on each node (port 7007)\n  - Uses `SwarmClientManager` for node discovery\n- **K8s mode**: Pod log monitoring in Kubernetes cluster\n  - Implements `container.Client` interface via Kubernetes API\n- **Agent mode**: Lightweight gRPC agent for remote log collection\n  - Run with `dozzle agent` or `bun run agent:dev`\n  - Listens on port 7007 with TLS certificate authentication\n\n## Key Architectural Patterns\n\n### Backend Abstraction Layers\n\nThe backend follows a clean layered architecture:\n\n```\nHTTP Handlers (internal/web)\n    ↓\nHostService Interface (internal/hostservice)\n    ↓\ncontainer.ClientService Interface (per host)\n    ↓\ncontainer.Client Interface\n    ↓\nImplementation (internal/container/{docker,k8s,agent})\n```\n\n**When adding new container operations:**\n\n1. Define method in `container.Client` interface (`internal/container/client.go`)\n2. Implement in `internal/container/docker/client.go` (and `internal/container/k8s/client.go` if applicable)\n3. Add it to `container.ClientService` (`internal/container/client_service.go`) and implement it in each\n   platform's `service.go` (`docker/`, `k8s/`, `agent/`)\n4. Add HTTP handler in `internal/web/` with appropriate route\n\n### Frontend Data Flow\n\n**Real-time Log Viewing:**\n\n1. User navigates to `/container/{id}` route\n2. Page component calls `useContainerStream(container)` composable\n3. Composable creates EventSource connection to `/api/containers/logs/stream?container={host}/{id}` (repeated for a merged view)\n4. Backend streams `LogEvent` objects via SSE\n5. Frontend buffers events (250ms debounce, max 1000ms)\n6. Batched buffer flushes update reactive `messages` array\n7. `LogViewer.vue` renders using appropriate component (`SimpleLogItem`, `ComplexLogItem`, `GroupedLogItem`)\n8. When messages exceed `maxLogs` (400), oldest entries replaced or marked as `SkippedLogsEntry`\n\n**Stats Streaming:**\n\n1. `container.ts` store connects to `/api/events/stream` on app init\n2. Backend multiplexes container events and stats into single SSE stream\n3. `container-stat` events update `Container._stat` and append to `_statsHistory`\n4. EMA calculation provides smoothed `movingAverageStat` (alpha=0.2)\n5. `ContainerTable.vue` displays mini bar charts using `statsHistory` with downsampling\n\n### Protocol Buffer Flow (Agent Mode)\n\n1. Main server creates `agent.NewClient(endpoint, certs)` for each remote host\n2. AgentClient implements `container.Client` interface\n3. Method calls translate to gRPC requests defined in `protos/rpc.proto`\n4. Remote agent receives gRPC call, delegates to local `docker.Client`\n5. Streaming RPCs (logs, stats, events) use bidirectional channels\n6. Responses converted back to domain models in `internal/container/agent/convert.go`\n\n### Cloud Tool Execution Flow\n\n1. `cloud.Client.Run()` blocks until `Notify()` signals a cloud dispatcher is configured\n2. `connect()` establishes bidirectional gRPC stream (`ToolStream` RPC) to cloud endpoint\n3. Cloud sends `ToolRequest` (ListTools or CallTool), client dispatches via `executeTool()`\n4. Tool calls run concurrently (max 5 via weighted semaphore), responses sent back on stream\n5. On disconnect, exponential backoff (1s→30s with jitter) triggers reconnection\n6. `PermissionDenied` errors stop retrying permanently (invalid API key / no pro plan)\n7. Tool definitions cached via `sync.Once`; zero overhead for non-cloud users\n\n### Log Parsing Pipeline\n\n1. Docker API returns multiplexed stream (8-byte headers + payload)\n2. `container/docker/log_reader.go` parses headers, extracts stdout/stderr type\n3. `container/logparse/event_generator.go` receives raw log lines\n4. Detection logic identifies:\n   - JSON structure → `ComplexLogEntry`\n   - Multi-line patterns (stack traces) → `GroupedLogEntry`\n   - Single lines → `SimpleLogEntry`\n5. Log level extraction via regex patterns\n6. `LogEvent` serialized to JSON and sent via SSE\n7. Frontend deserializes and renders with appropriate component\n\n## Adding New Features\n\n### Adding a New HTTP Route\n\n1. Define route in `internal/web/routes.go` using chi router:\n   ```go\n   r.Get(\"/api/custom-endpoint\", h.customHandler)\n   ```\n2. Implement handler method in appropriate file (e.g., `actions.go`, `logs.go`)\n3. Use `hostService` to find container/host via `FindContainer()` or `FindHost()`\n4. Return JSON response or establish SSE/WebSocket stream\n\n### Adding a New Log View Type\n\n1. Create route file in `assets/pages/` (e.g., `custom/[id].vue`)\n2. Create composable in `assets/composable/logs/eventStreams.ts` (e.g., `useCustomStream()`)\n3. Composable should:\n   - Build API URL with appropriate filters\n   - Create EventSource connection\n   - Handle buffering and message batching\n   - Return reactive `messages` array and control methods\n4. Use `LogViewer.vue` component to render messages\n5. Add backend API endpoint if needed (see above)\n\n### Adding Container Stats/Metrics\n\n1. Add field to `Stat` type in `internal/container/types.go`\n2. Update `container/docker/stats_collector.go` (and `container/k8s/stats_collector.go`) to extract the metric\n3. Add calculation logic in `container/docker/calculation.go` if needed\n4. Ensure protobuf definition includes field in `protos/rpc.proto`, and map it in `container/agent/convert.go`\n5. Frontend automatically receives updates via existing SSE stream\n6. Update `Container` model in `assets/models/Container.ts` if UI needs access\n\n### Working with Notifications/Alerts\n\n**Backend** (`internal/notification/`):\n\n- `manager.go`: Rule evaluation engine, manages alert state\n- `log_listener.go`: Subscribes to container log streams, evaluates rules against incoming logs\n- `types.go`: Alert rule definitions (log pattern matching, thresholds)\n- `dispatcher/`: Notification channel implementations\n\n**Frontend** (`assets/pages/notifications.vue`, `assets/components/notifications/`):\n\n- `AlertForm.vue`, `DestinationForm.vue`: UI for creating rules\n- Rules persisted to `./data/notifications.yml` via `internal/notification/persist.go`\n- Alert state displayed in notification cards\n\n**Adding a new notification channel:**\n\n1. Implement dispatcher interface in `internal/notification/dispatcher/`\n2. Register in `manager.go` dispatcher factory\n3. Add UI form in `assets/components/notifications/DestinationForm.vue`\n\n### Adding a New Cloud Tool\n\n1. Define the tool in `AvailableTools()` in `internal/cloud/tools.go` with name, description, and parameter schema\n2. Add a response message type in `protos/cloud.proto` and add it to the `CallToolResponse.result` oneof\n3. Run `make generate` to regenerate protobuf code\n4. Add a case in the `executeTool()` switch in `internal/cloud/tools.go`\n5. Implement the execution function in the appropriate `tools_*.go` file\n6. Use `ToolHostService` interface methods to access container/host data\n7. Add tests in `tools_test.go`\n\n## Common Development Patterns\n\n### Testing\n\n- Always run Go tests with race detector: `go test -race`\n- Frontend tests require `TZ=UTC` for timestamp consistency\n- Integration tests use Playwright with `make int` (runs docker-compose setup)\n- Use `testify/assert` for Go test assertions\n\n### Hot Reload Development\n\n- `make dev` runs both backend (air) and frontend (vite) with hot reload\n- `DEV=true` disables embedded asset serving\n- `LIVE_FS=true` serves assets from filesystem instead of embedded\n- Backend changes trigger air restart automatically\n- Frontend changes trigger vite HMR\n\n### Debugging\n\n- Backend logs: Set `--level debug` flag or `DOZZLE_LEVEL=debug` env var\n- Frontend: Vue DevTools browser extension\n- SSE streams: Browser DevTools Network tab shows EventSource connections\n"},"files":{".claude/agents/go-perf-reviewer.md":"---\nname: go-perf-reviewer\ndescription: \"Use this agent when Go code has been written or modified and needs review for performance issues, inefficient patterns, or unnecessary resource loading. This agent focuses exclusively on Go backend code and should be triggered after changes to `.go` files.\\\\n\\\\nExamples:\\\\n\\\\n- user: \\\"I just added a new endpoint that fetches container stats\\\"\\\\n  assistant: \\\"Let me use the go-perf-reviewer agent to check the new endpoint for performance issues.\\\"\\\\n  <commentary>Since Go code was written for a new endpoint, use the Task tool to launch the go-perf-reviewer agent to review the changes for inefficient patterns.</commentary>\\\\n\\\\n- user: \\\"Can you review my changes to the Docker client wrapper?\\\"\\\\n  assistant: \\\"I'll launch the go-perf-reviewer agent to analyze your Docker client changes for performance concerns.\\\"\\\\n  <commentary>The user is asking for a review of Go code changes. Use the Task tool to launch the go-perf-reviewer agent to identify any performance anti-patterns.</commentary>\\\\n\\\\n- user: \\\"I refactored the log streaming pipeline\\\"\\\\n  assistant: \\\"Let me run the go-perf-reviewer agent against your refactored streaming code to catch any performance regressions.\\\"\\\\n  <commentary>Streaming code is performance-critical. Use the Task tool to launch the go-perf-reviewer agent to ensure the refactor doesn't introduce inefficiencies.</commentary>\"\nmodel: opus\ncolor: green\nmemory: project\n---\n\nYou are an expert Go performance engineer with deep knowledge of runtime internals, memory allocation patterns, garbage collector behavior, and idiomatic high-performance Go. You specialize in reviewing Go code for performance anti-patterns in long-running server applications that interact with Docker, gRPC, and streaming APIs.\n\n**Core Philosophy**: Dozzle must be lean and lazy by default. It should never load, allocate, compute, or fetch anything until it is actually needed. Every byte of memory and every CPU cycle matters in a lightweight monitoring tool.\n\n## Your Review Process\n\n1. **Examine only the changed Go files**. Use available tools to read the recent changes or diffs.\n2. **Identify specific performance issues** — do not comment on style, naming, or correctness unless it directly causes a performance problem.\n3. **Provide actionable feedback** with file:line references and concrete fix suggestions.\n\n## Patterns to Flag\n\n### Memory & Allocation\n\n- Unnecessary allocations in hot paths (loops, stream handlers, per-request code)\n- Slice/map pre-allocation missing when size is known or estimable\n- `append` in tight loops without pre-sized slices\n- String concatenation with `+` instead of `strings.Builder` in loops\n- Returning large structs by value instead of pointer when appropriate\n- Unnecessary copies of large data (e.g., ranging over slice of structs by value)\n- `[]byte` ↔ `string` conversions that could be avoided\n- Creating closures in loops that capture loop variables unnecessarily\n- Allocating buffers per-call that could be pooled via `sync.Pool`\n\n### Lazy Loading & Eager Initialization\n\n- **Top priority**: Loading data, making API calls, or initializing resources before they are actually needed\n- Fetching all containers/stats when only a subset is requested\n- Initializing clients, connections, or caches at startup that may never be used\n- Reading entire files/streams into memory when streaming/pagination would suffice\n- Computing derived data eagerly when it could be computed on demand\n- Pre-populating maps/caches with all possible entries instead of lazy-filling\n\n### Concurrency & Goroutines\n\n- Goroutine leaks: goroutines without proper cancellation via `context.Context`\n- Missing `defer cancel()` after `context.WithCancel/WithTimeout`\n- Unbounded goroutine spawning without semaphore/worker pool\n- Channel misuse: unbuffered channels causing unnecessary blocking, or oversized buffered channels wasting memory\n- Holding locks longer than necessary; lock contention in hot paths\n- Using `sync.Mutex` where `sync.RWMutex` would reduce contention\n\n### I/O & Streaming\n\n- Not using `bufio.Reader`/`bufio.Writer` for I/O operations\n- Reading entire HTTP response bodies into memory (`io.ReadAll`) when streaming is possible\n- Not closing response bodies, readers, or connections (resource leaks)\n- Blocking I/O without timeouts or context cancellation\n- Serializing/deserializing JSON repeatedly when it could be done once\n- Using `encoding/json` in ultra-hot paths where a faster serializer is warranted\n\n### Docker/gRPC Specific\n\n- Making redundant Docker API calls (e.g., inspecting a container multiple times)\n- Not using Docker API filters to narrow results server-side\n- Fetching all container logs when `tail` or `since` parameters should limit scope\n- gRPC streams not properly drained or closed\n- Creating new Docker/gRPC clients per request instead of reusing\n\n### General Go Anti-Patterns\n\n- `reflect` usage in hot paths\n- `fmt.Sprintf` for simple string operations where direct concatenation suffices\n- `interface{}` / `any` boxing causing heap escapes\n- Unnecessary use of `defer` in tight loops (small but real overhead)\n- `time.After` in select loops (creates new timer each iteration; use `time.NewTimer` + `Reset`)\n- Regex compilation inside functions instead of package-level `var` with `regexp.MustCompile`\n- Sorting large slices repeatedly instead of maintaining sorted order\n\n## Output Format\n\nUse ultra-brief mode as specified by the project:\n\n- Critical performance issues only\n- Brief bullet points with file:line references\n- Concrete suggestion for each issue\n- Maximum ~10-15 lines per response\n- No praise sections, no summaries, no fluff\n\nExample output:\n\n```\n- `internal/container/docker/client.go:142` — `io.ReadAll(resp.Body)` reads entire log stream into memory. Stream with `bufio.Scanner` instead.\n- `internal/web/logs.go:87` — New `json.Encoder` created per log line in hot loop. Reuse encoder or use `sync.Pool`.\n- `internal/hostservice/multi_host.go:53` — All hosts initialized eagerly at startup. Defer client creation until first access.\n```\n\nIf no performance issues are found, state that clearly in one line.\n\n**Update your agent memory** as you discover performance patterns, hot paths, allocation-heavy code paths, and architectural decisions in this codebase. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.\n\nExamples of what to record:\n\n- Hot paths identified (log streaming, stats collection, event processing)\n- Existing `sync.Pool` usage or buffer reuse patterns\n- Known allocation-heavy areas\n- Docker API call patterns and caching strategies\n- gRPC streaming patterns used in agent mode\n- Areas where lazy loading is already implemented vs. missing\n\n# Persistent Agent Memory\n\nYou have a persistent Persistent Agent Memory directory at `/Users/araminfar/Workspace/dozzle/.claude/agent-memory/go-perf-reviewer/`. Its contents persist across conversations.\n\nAs you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.\n\nGuidelines:\n\n- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise\n- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md\n- Update or remove memories that turn out to be wrong or outdated\n- Organize memory semantically by topic, not chronologically\n- Use the Write and Edit tools to update your memory files\n\nWhat to save:\n\n- Stable patterns and conventions confirmed across multiple interactions\n- Key architectural decisions, important file paths, and project structure\n- User preferences for workflow, tools, and communication style\n- Solutions to recurring problems and debugging insights\n\nWhat NOT to save:\n\n- Session-specific context (current task details, in-progress work, temporary state)\n- Information that might be incomplete — verify against project docs before writing\n- Anything that duplicates or contradicts existing CLAUDE.md instructions\n- Speculative or unverified conclusions from reading a single file\n\nExplicit user requests:\n\n- When the user asks you to remember something across sessions (e.g., \"always use bun\", \"never auto-commit\"), save it — no need to wait for multiple interactions\n- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files\n- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project\n\n## MEMORY.md\n\nYour MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.\n",".claude/skills/update-int-snapshots/SKILL.md":"---\nname: update-int-snapshots\ndescription: Use when Playwright visual snapshots in e2e/visual.spec.ts-snapshots/ need to be regenerated after a UI change (logo, theme, layout, anything that affects rendered pixels) and the integration suite needs to be rerun via make int\n---\n\n# Update Integration Snapshots\n\n## When to Use\n\n- A visual change landed (logo, color, spacing, fonts) and `make int` will fail on `visual.spec.ts` until snapshots are regenerated.\n- A snapshot test reports diffs you've confirmed are intentional.\n\nDo NOT use when diffs are unintentional regressions — investigate first.\n\n## Procedure\n\n1. **Delete the stale snapshots** so Playwright writes fresh ones (don't try to update in place, the old PNGs can confuse the diff):\n\n   ```bash\n   rm e2e/visual.spec.ts-snapshots/*.png\n   ```\n\n2. **Run with `--update-snapshots` via `compose up`.** You must use `compose up` (not `compose run`) because the navigation sidebar snapshot includes the live container list, and the two modes produce different sibling containers. The cleanest way is to patch the playwright command in `docker-compose.yml` temporarily:\n\n   ```bash\n   sed -i.bak 's|command: npx --yes playwright test|command: npx --yes playwright test --update-snapshots|' docker-compose.yml\n   make int\n   mv docker-compose.yml.bak docker-compose.yml\n   ```\n\n   Snapshots must be generated in Linux/Chromium (the compose image), not macOS, because filenames include the platform suffix (e.g. `-chromium-linux.png`).\n\n   Nothing needs clearing first. The compose file publishes no host ports, every container is named `dozzle_e2e_*` under the fixed project `dozzle_e2e`, and filters match those names exactly. Other containers on the machine can't block the run or show up in the snapshots, and a leftover run from another worktree is recreated. Two worktrees running `make int` at the same time do still clobber each other, so run one at a time.\n\n3. **Verify** by rerunning the normal suite:\n\n   ```bash\n   make int\n   ```\n\n   Should now pass cleanly.\n\n4. **Commit the regenerated PNGs** alongside the UI change so CI stays green.\n\n## Common Mistakes\n\n- **Running `npx playwright test --update-snapshots` locally on macOS.** Generates `-darwin` filenames CI doesn't use. Always go through the docker compose image.\n- **Forgetting to delete first.** `--update-snapshots` does overwrite, but if the test layout changed (new test, renamed snapshot), stale PNGs are left behind. Wipe-and-regenerate is safest.\n- **Adding a test container without the prefix, or a filter without anchors.** `name=dozzle` is a substring match and pulls in any local container with \"dozzle\" in its name. Use `container_name: dozzle_e2e_<service>` and `DOZZLE_FILTER=name=^dozzle_e2e_<service>$$` (`$$` escapes `$` in compose).\n- **Publishing a host port \"for debugging\".** It brings back conflicts with whatever else is on the machine. Reach instances by service name from inside the network.\n\n## Quick Reference\n\n```bash\nrm e2e/visual.spec.ts-snapshots/*.png\nsed -i.bak 's|command: npx --yes playwright test|& --update-snapshots|' docker-compose.yml\nmake int\nmv docker-compose.yml.bak docker-compose.yml\nmake int   # verify pass\n```\n",".claude/agents/bug-hunter.md":"---\nname: bug-hunter\ndescription: \"Use this agent when you need to review recently written or modified code for bugs, logic errors, edge cases, and unexpected behavior. This includes both Go backend code and Vue/TypeScript frontend code. This agent should be used after writing new features, fixing bugs, or refactoring code to catch issues before they reach production.\\\\n\\\\nExamples:\\\\n\\\\n- User writes a new HTTP handler in Go:\\\\n  user: \\\"I just added a new endpoint for container health checks\\\"\\\\n  assistant: \\\"Let me review the new code for potential bugs and edge cases.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to review the recently changed files>\\\\n\\\\n- User implements a new Vue composable:\\\\n  user: \\\"I created a new composable for managing WebSocket reconnection\\\"\\\\n  assistant: \\\"I'll launch the bug hunter to review your new composable for edge cases and potential issues.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to analyze the composable>\\\\n\\\\n- User modifies log parsing logic:\\\\n  user: \\\"I updated the event_generator.go to handle a new log format\\\"\\\\n  assistant: \\\"Let me have the bug hunter review your changes to ensure all edge cases in log parsing are covered.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to review the modified parsing code>\\\\n\\\\n- After a chunk of code is written during a feature implementation:\\\\n  assistant: \\\"I've finished implementing the notification dispatcher. Let me run the bug hunter to check for issues.\\\"\\\\n  <uses Task tool to launch bug-hunter agent proactively to review the new code>\"\nmodel: opus\ncolor: blue\nmemory: project\n---\n\nYou are an elite bug-hunting code reviewer with deep expertise in both Go backend systems and Vue 3/TypeScript frontend applications. You have extensive experience finding subtle bugs, race conditions, nil pointer dereferences, unhandled edge cases, type safety issues, and logic errors that slip past typical review. You think adversarially — always asking \"what could go wrong here?\"\n\n## Your Mission\n\nReview recently written or modified code to find bugs, logic errors, and uncovered edge cases. You focus on code that could fail at runtime, produce incorrect results, or behave unexpectedly under specific conditions.\n\n## Review Process\n\n1. **Identify Changed/New Code**: Use git status, git diff, or examine the files the user points you to. Focus on recently written code, not the entire codebase.\n\n2. **Analyze Each File Systematically**: For every file with changes, examine:\n   - Control flow paths (all branches of if/else, switch, select)\n   - Error handling (are errors checked? propagated correctly? logged?)\n   - Nil/undefined checks (pointer dereference in Go, optional chaining in TS)\n   - Concurrency safety (goroutine leaks, race conditions, channel operations)\n   - Resource cleanup (deferred closes, stream cleanup, event listener removal)\n   - Boundary conditions (empty arrays, zero values, max values, negative numbers)\n   - Type safety (type assertions in Go, type narrowing in TS)\n   - API contract adherence (correct HTTP status codes, proper SSE format, GraphQL schema alignment)\n\n3. **Cross-Layer Analysis**: Check that frontend and backend changes are consistent:\n   - API request/response shapes match between Go handlers and TypeScript types\n   - SSE event names and payload structures align\n   - GraphQL schema, resolvers, and frontend queries are in sync\n   - WebSocket message formats match on both sides\n\n## Go-Specific Bug Patterns to Check\n\n- **Nil pointer dereference**: Especially after type assertions, map lookups, and interface conversions\n- **Goroutine leaks**: Goroutines blocked on channels that are never closed or written to\n- **Race conditions**: Shared state accessed from multiple goroutines without synchronization\n- **Deferred closure in loops**: `defer` inside loops won't execute until function returns\n- **Error shadowing**: Using `:=` that shadows an outer `err` variable\n- **Slice/map initialization**: Operating on nil slices/maps (nil map write panics)\n- **Context cancellation**: Not respecting context.Done() in long-running operations\n- **HTTP response body leaks**: Not closing response bodies after HTTP calls\n- **Integer overflow**: Especially in stats calculations with uint64\n- **String/byte slice sharing**: Modifying a slice that shares underlying array\n- **Channel operations on nil channels**: Blocking forever on nil channel send/receive\n- **Mutex copy**: Passing sync.Mutex by value instead of pointer\n\n## Vue/TypeScript-Specific Bug Patterns to Check\n\n- **Reactive reference unwrapping**: Using `.value` correctly with `ref()` vs `reactive()`\n- **Computed dependency tracking**: Missing reactive dependencies in computed properties\n- **Watch cleanup**: Not cleaning up watchers, event listeners, or timers in `onUnmounted`\n- **SSE/EventSource cleanup**: Connections not properly closed on component unmount\n- **Array reactivity**: Using index assignment instead of reactive methods\n- **Optional chaining gaps**: Accessing nested properties without null checks\n- **Promise error handling**: Unhandled promise rejections, missing `.catch()` or try/catch\n- **Type narrowing issues**: Assuming a type without proper guards\n- **Stale closure references**: Callbacks capturing outdated reactive values\n- **Memory leaks**: Growing arrays without bounds (check maxLogs enforcement, statsHistory rolling window)\n- **Race conditions in async operations**: Component unmounted before async operation completes\n- **Incorrect v-if/v-show usage**: Rendering components that depend on data not yet loaded\n- **Event buffer overflow**: Not handling backpressure in SSE streams\n\n## Project-Specific Concerns\n\n- **EMA calculations**: Alpha=0.2 for stats smoothing — verify division by zero, NaN handling\n- **Rolling window (300 items)**: Ensure proper eviction and no off-by-one errors\n- **Log entry type discrimination**: `LogEntry.create()` must handle all `logEvent.t` values\n- **Multi-host operations**: Container/host lookups via `FindContainer()`/`FindHost()` may return nil\n- **Docker API version negotiation**: Calls may fail on older Docker versions\n- **Protobuf serialization**: `FromProto()` methods must handle nil/empty fields\n- **Authentication modes**: Code must work correctly in all three auth modes (none, simple, forward-proxy)\n- **SSE buffering**: 250ms debounce with 1000ms max — verify timer cleanup\n- **CPU normalization**: Division by `cpuLimit` or `nCPU` — check for zero values\n\n## Output Format\n\nFollow ultra-brief mode as specified in the project guidelines:\n\n- Critical issues only (bugs, security, blockers)\n- Brief bullet points, no lengthy explanations\n- Skip verbose sections (no \"Strengths\", \"Summary\", etc.)\n- Include file:line references when relevant\n- Maximum ~10-15 lines per response\n\nFor each bug found, report:\n\n- **File:line** — Brief description of the bug\n- **Severity**: 🔴 Critical (will crash/corrupt) | 🟡 Warning (could fail under specific conditions) | 🟠 Edge case (uncovered scenario)\n- One-line fix suggestion if obvious\n\nIf no bugs are found, say so concisely. Do not fabricate issues.\n\n## Approach\n\n1. First, determine what code was recently changed (check git diff or ask the user)\n2. Read the changed files carefully\n3. For each file, apply the relevant bug pattern checklist above\n4. Cross-reference frontend and backend changes for consistency\n5. Report findings in the ultra-brief format\n\nDo NOT review code style, naming conventions, or suggest refactors unless they mask a bug. Focus exclusively on correctness.\n\n**Update your agent memory** as you discover recurring bug patterns, common error-prone code paths, areas of the codebase with historical issues, and edge cases specific to this project's architecture. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.\n\nExamples of what to record:\n\n- Recurring nil-check omissions in specific packages\n- Components that frequently have cleanup issues\n- API endpoints with known edge case gaps\n- Stats calculation patterns that are error-prone\n- Log parsing paths that have caused issues before\n\n# Persistent Agent Memory\n\nYou have a persistent Persistent Agent Memory directory at `/Users/araminfar/Workspace/dozzle/.claude/agent-memory/bug-hunter/`. Its contents persist across conversations.\n\nAs you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.\n\nGuidelines:\n\n- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise\n- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md\n- Update or remove memories that turn out to be wrong or outdated\n- Organize memory semantically by topic, not chronologically\n- Use the Write and Edit tools to update your memory files\n\nWhat to save:\n\n- Stable patterns and conventions confirmed across multiple interactions\n- Key architectural decisions, important file paths, and project structure\n- User preferences for workflow, tools, and communication style\n- Solutions to recurring problems and debugging insights\n\nWhat NOT to save:\n\n- Session-specific context (current task details, in-progress work, temporary state)\n- Information that might be incomplete — verify against project docs before writing\n- Anything that duplicates or contradicts existing CLAUDE.md instructions\n- Speculative or unverified conclusions from reading a single file\n\nExplicit user requests:\n\n- When the user asks you to remember something across sessions (e.g., \"always use bun\", \"never auto-commit\"), save it — no need to wait for multiple interactions\n- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files\n- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project\n\n## MEMORY.md\n\nYour MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.\n","CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Response Style\n\nKeep chat responses brief: bullet points, `file:line` references, no \"Strengths\" or\n\"Summary\" sections. What the PR review bot looks for is set in\n`.github/workflows/claude-code-review.yml`, not here.\n\n## Translations\n\n`locales/en.yml` is the source of truth, but it is never the only file to change. Whenever you add, rename, or remove a key in `en.yml`, apply the same change to **every** other file in `locales/` in the same commit, with the string actually translated (not the English text copied over). Keys are also deleted everywhere when they lose their last consumer.\n\nThere is no Crowdin or Weblate sync in this repo, so a key that only exists in `en.yml` silently falls back to English for every other locale and nothing ever flags it.\n\n### Docs Translations\n\n`docs/` is translated into `zh`, `de`, `fr` and `es`. The English page under `docs/` is the source of truth, and `docs/<locale>/<same path>` mirrors it. Changing an English page means updating all four translations in the same commit.\n\nUnlike `locales/`, there is no fallback here. A stale translated page renders confidently wrong instructions rather than quietly showing English, so drift is worse than a missing key.\n\nEach translated file carries a `sourceHash` in its frontmatter recording the English source it was written from. `bun docs/scripts/check-translations.mjs` fails when they diverge, and runs in CI as the `Docs Translations` job. After actually translating the changed prose, re-stamp with:\n\n```bash\nbun docs/scripts/check-translations.mjs --update\n```\n\n`--update` only re-stamps hashes. It does not translate anything, so running it on an untranslated page turns CI green while leaving the page wrong. Translate first.\n\nInside translated pages, everything in a fenced code block stays byte-identical to English except natural-language comments, and internal links are locale-prefixed (`/guide/agent` becomes `/de/guide/agent`). The sidebar is not duplicated per locale: its structure lives in `docs/.vitepress/locales/structure.ts` and each locale file supplies only labels, so a new page needs one slug there plus one label per locale.\n\n## Testing Unreleased PRs\n\nWhen replying to a GitHub issue or discussion where the fix lives in an open PR, ask the reporter to test the pre-built image: `amir20/dozzle:pr-XXX` (XXX = PR number). CI builds a tagged image per PR, so reporters can verify without waiting for the next release.\n\n## GitHub Tone (issues, PRs, comments, discussions)\n\nWhen posting anything to GitHub, write like a human maintainer, not an AI assistant. Avoid telltale LLM patterns:\n\n- No em dashes or en dashes. Use commas, periods, or parentheses instead.\n- No \"Not X, but Y\" rhetorical contrasts.\n- No throat-clearing openers (\"Great point\", \"Makes sense\", \"Thanks for the detailed write-up\").\n- No closing summaries or recap sentences.\n- No bolded inline labels mid-paragraph (\"**Why:**\", \"**Note:**\").\n- Drop hedges (\"essentially\", \"basically\", \"essentially just\"). Say it plain.\n- Lowercase casual tone is fine. Contractions are fine. Short sentences are fine.\n- Don't over-explain tradeoffs. State the decision, give one reason, stop.\n\n## Scope Discipline\n\nSmall first versions win. Building is cheap here; reviewing and maintaining is not.\n\n- **Agree the scope as a changelog before any code.** For a new feature, write what\n  ships (user-visible bullets) and an explicit \"Not in this release\" list, and get it\n  approved. Anything not on the ship list is out of scope, however reasonable it looks.\n- **A plan is a menu, not a work order.** Build phase 1 only. Later phases start after\n  phase 1 has merged, unless the user asks for more.\n- **Reuse before you add.** A new table, RPC, proto field, flag, label or setting needs a\n  one-line reason why an existing one (labels, existing events, existing alert channels,\n  existing pages) can't do it. Prefer one delivery surface to several.\n- **Stop at ~1,000 lines.** If a PR's non-test, non-generated diff passes ~1,000 lines,\n  stop and check scope with the user before continuing.\n- **One feature, one PR per repo.** Unrelated work (e.g. a settings redesign during an\n  updates feature) gets its own PR off main, never a layer in the same stack. Avoid\n  stacks deeper than two; with a GitHub stack, merge from the bottom.\n- **Subagents and workflows get the out-of-scope list.** Every implementation prompt\n  includes \"not in scope\" and the rule: a deviation that adds behaviour is reported,\n  not built.\n- **Review fixes fix; they don't extend.** Addressing feedback must not add new\n  mechanisms. If a fix needs one, raise it as a scope question.\n- **Audit size before review, not after.** When a body of work is done, report its size\n  per feature (code, tests, generated) next to its user impact, so cuts happen before\n  review time is spent.\n\n## Project Overview\n\nDozzle is a lightweight, web-based Docker log viewer with real-time monitoring capabilities. It's a hybrid application with:\n\n- **Backend**: Go (HTTP server, Docker API client, WebSocket streaming)\n- **Frontend**: Vue 3 (SPA with Vite, TypeScript)\n\nThe application supports multiple deployment modes: standalone server, Docker Swarm, and Kubernetes (k8s).\n\n## Development Commands\n\n### Setup\n\n```bash\n# Install dependencies\nbun install\n\n# Generate certificates and protobuf files\nmake generate\n```\n\n### Development\n\n```bash\n# Run full development environment (backend + frontend with hot reload)\nmake dev\n\n# Same, on a free port trio derived from this checkout's path, so several worktrees\n# can each run an instance at once. Prints the URL it picked.\nmake dev-auto\n\n# Alternative: Run backend and frontend separately\nbun run watch:backend  # Go backend with air (port 3100)\nbun run watch:frontend # Vite dev server (port 3100)\n\n# Run in agent mode for development\nbun run agent:dev\n```\n\n### Building\n\n```bash\n# Build frontend assets\nbun run build\n# or\nmake dist\n\n# Build entire application (includes frontend build)\nmake build\n\n# Build Docker image\nmake docker\n```\n\n### Testing\n\n```bash\n# Run Go tests\nmake test\n\n# Run frontend tests (Vitest)\nbun run test\n# Run in watch mode\nTZ=UTC bun run test --watch\n\n# Type checking\nbun run typecheck\n\n# Lint (Tailwind classes only)\nbun run lint\nbun run lint --fix\n```\n\nAlways go through `bun run` for `test` and `build`. `bun test` and `bun build` are\nbun's own test runner and bundler, not the scripts in `package.json`, and they fail\nin confusing ways against this codebase.\n\n`bun run` does not mean the bun runtime: it follows each tool's `#!/usr/bin/env node`\nshebang unless the script says `bun --bun`. Every script in `package.json` that starts a\nJS tool carries that flag except `test` and `typecheck`, which need node: under `--bun` vitest's jsdom\nworkers fail to start and `vue-tsc` reports a false TS2614. `release` (bumpp) is\nuntested on bun and also left alone. Those are the only reasons node is still required.\n\n### Linting\n\nESLint here checks Tailwind classes and nothing else. It is not a general JS or TS\nlinter, and it should not become one: `tsconfig.json` already has `strict`,\n`strictNullChecks` and `noUnusedLocals`, so `vue-tsc` covers that ground, and prettier\nplus `prettier-plugin-tailwindcss` own formatting and class order. No stylistic rule\nbelongs in `eslint.config.js`, which is also why `eslint-config-prettier` is not needed.\n\nEvery rule the Tailwind plugin ships is listed in that config with the reason it is on\nor off, so add to that list rather than reaching for a preset.\n\n`eslint --fix` runs on staged `js`/`mjs`/`ts`/`mts`/`vue` in the pre-commit hook, before\nprettier so prettier gets the last word on formatting. Run `bun run lint --fix` by hand\nafter writing a batch of markup if you would rather not find out at commit time.\n\nTwo blind spots worth knowing, because a green run does not mean a clean tree:\n\n- `@apply` inside an SFC `<style>` block is never handed to the plugin, so a bad class\n  there is never reported.\n- The arbitrary-value rule only maps whole-number spacing steps. `gap-[2px]` and `w-[3px]`\n  go unreported even though `gap-0.5` and `w-0.75` exist.\n\n### Preview & Other\n\n```bash\n# Preview production build locally\nbun run preview\n# or\nmake preview\n\n# Run integration tests (Playwright)\nmake int\n```\n\n## Architecture\n\n### Backend (Go)\n\nThe Go backend is organized into these key packages:\n\n- **`internal/web/`** - HTTP server and routing layer\n  - Routes defined in `routes.go` using chi router\n  - WebSocket/SSE handlers for log streaming (`logs.go`)\n  - Authentication middleware and token management (`auth.go`)\n  - Container action handlers (`actions.go`)\n  - `sse/`: the SSE writer; `search/`: log filtering, highlighting and HTML escaping\n\n- **`internal/container/`** - The generic layer every platform implements\n  - `types.go`, `host.go`: domain models (`Container`, `Host`, `LogEvent`, `ContainerStat`)\n  - `client.go`: `container.Client`, the raw engine interface\n  - `client_service.go`: `container.ClientService`, the per-host contract the rest of the app uses\n  - `store.go`: the container cache docker and k8s share (its header comment explains who\n    may write to the map), split by concern into `store_events.go` (the event loop),\n    `store_refresh.go` (reconnect, list and reconcile) and `store_fanout.go`\n    (bounded delivery to subscribers)\n  - `collector_lifecycle.go`: the lazy start/idle-stop reference count both stats collectors use\n  - `logparse/`: the log pipeline both platforms feed (`event_generator.go` grouping and JSON\n    detection, `level_guesser.go`, `logfmt.go`, `timestamp_prefix.go`)\n  - One folder per platform, each holding that platform end to end:\n    - `docker/`: `client.go`, `log_reader.go`, `stats_collector.go`, and `service.go` (`docker.Service`)\n    - `k8s/`: `client.go`, `log_reader.go`, `stats_collector.go`, and `service.go` (`k8s.Service`)\n    - `agent/`: gRPC `client.go`/`server.go`, `convert.go` (proto conversion), `service.go`, and\n      generated `pb/` (protos in `protos/`)\n\n- **`internal/hostservice/`** - Orchestration across hosts, one per deployment mode\n  - `multi_host.go`: `MultiHostService` (server and swarm modes)\n  - `retriable_client_manager.go` (server), `swarm_client_manager.go` (swarm)\n  - `k8s_cluster.go`: `K8sClusterService` (k8s mode)\n\n- **`internal/cli/`** - Command-line parsing, subcommands, client wiring (`clients.go`)\n\n- **`internal/cloud/`** - Dozzle Cloud integration (tool execution engine)\n  - `client.go`: Bidirectional gRPC stream client with auto-reconnect and exponential backoff\n  - `tools.go`: Tool registration, dispatch (`executeTool`), and `ToolHostService` interface\n  - `tools_containers.go`: Container listing, finding, stats, and inspection tools\n  - `tools_logs.go`: Log fetching with level/query/regex filtering (max 100 lines)\n  - `tools_actions.go`: Container start/stop/restart actions (gated by `enableActions`)\n  - `tools_helpers.go`: Proto conversion utilities and host name resolution\n  - Uses `protos/cloud.proto` for service and message definitions\n\n- **`internal/auth/`** - Authentication providers\n  - Simple file-based auth (`simple.go`)\n  - Forward proxy auth (`proxy.go`)\n  - Role-based authorization (`roles.go`)\n\n- **`internal/notification/`** - Alert and notification system\n  - `manager.go`: Notification rule evaluation and dispatching\n  - `log_listener.go`: Log pattern matching for alerts\n  - `dispatcher/`: Notification channel implementations (email, webhook, etc.)\n\n- **`main.go`** - Application entry point with mode switching (server/swarm/k8s/agent)\n\n### Frontend (Vue 3)\n\nThe frontend uses file-based routing with these conventions:\n\n- **`assets/pages/`** - File-based routes (unplugin-vue-router)\n  - `container/[id].vue`: Single container view\n  - `merged/[ids].vue`: Multi-container merged view\n  - `host/[id].vue`: Host-level logs\n  - `service/[name].vue`: Swarm service logs\n  - `stack/[name].vue`: Docker stack logs\n  - `group/[name].vue`: Custom grouped logs\n\n- **`assets/components/`** - Vue components (auto-imported). See \"Where files go\" below.\n  - `ui/`: generic primitives (`BarChart.vue`, `MetricCard.vue`, `Popover.vue`, `UsageMeter.vue`, ...)\n  - `shell/`: app frame and the singletons `layouts/default.vue` mounts once\n  - `nav/`: everything inside the sidebar\n  - `search/`: the command palette and search surfaces\n  - `containers/`: `ContainerTable.vue`, the toolbar, the terminal\n  - `hosts/`: `HostCard.vue` and friends\n  - `logs/`: the stream, with `logs/entries/` holding one file per `LogEntry` type\n    (`SimpleLogItem.vue`, `ComplexLogItem.vue`, `GroupedLogItem.vue`,\n    `ContainerEventLogItem.vue`, `SkippedEntriesLogItem.vue`, `LoadMoreLogItem.vue`)\n  - `views/`: one file per route that renders a log view, mounted by `assets/pages/`\n  - `notifications/`: the rules the user wrote\n  - `cloud/`: memory, cross-instance and account, split into `rail/`, `chat/`, `history/`\n\n- **`assets/stores/`** - Pinia stores (auto-imported)\n  - `config.ts`: App configuration and feature flags (injected from backend HTML, frozen immutable)\n  - `container.ts`: Container state management with EventSource streaming (`/api/events/stream`)\n  - `hosts.ts`: Multi-host state\n  - `settings.ts`: User preferences (localStorage-backed via profileStorage)\n  - `pinned.ts`: Pinned container logs for side-by-side viewing\n  - `swarm.ts`, `k8s.ts`: Deployment mode-specific state\n  - `announcements.ts`: Feature announcements\n\n- **`assets/composable/`** - Vue composables (auto-imported), foldered to mirror the components\n  - `ui/`: `popover.ts`, `media.ts`, `timeTicker.ts` - no domain knowledge\n  - `app/`: shell and global session state (`drawer.ts`, `theme.ts`, `toast.ts`, ...)\n  - `logs/`: the stream pipeline (`eventStreams.ts` SSE with 250ms buffered flushing,\n    `historicalLogs.ts`, `logContext.ts` provide/inject, `scrollContext.ts`,\n    `visible.ts`, `duckdb.ts` for SQL over logs)\n  - `containers/`: `containerActions.ts`, `imageUpdate.ts`, ...\n  - `cloud/`, `notifications/`, `editor/`\n\n  The `AutoImport` `dirs` glob is `assets/composable/**`. It must stay recursive: a bare\n  directory is scanned one level deep, and nested composables silently stop being\n  auto-imported with no error. The `**` also globs `*.spec.ts`, which is harmless only\n  while no spec exports anything.\n\n- **`assets/modules/`** - Vue plugins\n  - `router.ts`: Vue Router configuration\n  - `pinia.ts`: Pinia store setup\n  - `i18n.ts`: Internationalization\n\n### Communication Flow\n\n1. **Real-time Logs**: Frontend establishes SSE connections to `/api/containers/logs/stream?container={host}/{id}` (repeated for a merged view)\n2. **Container Events**: SSE stream at `/api/events/stream` pushes container lifecycle events\n3. **Stats**: Real-time CPU/memory stats streamed via SSE alongside events\n4. **Actions**: POST to `/api/hosts/{host}/containers/{id}/actions/{action}` (start/stop/restart)\n5. **Terminal**: WebSocket connections for container attach/exec at `/api/hosts/{host}/containers/{id}/attach`\n\n### Build System\n\n- **Frontend**: Vite builds to `dist/` with manifest\n- **Backend**: Embeds `dist/` using Go embed directive\n- **Hot Reload**: In development, `DEV=true` disables embedded assets, `LIVE_FS=true` serves from filesystem\n- **Makefile**: Orchestrates builds and dependency generation\n\n## Dozzle and Dozzle Cloud\n\nCloud features are folded into the Dozzle UI rather than linked to. The split that\ndecides where a feature lands:\n\n> **Dozzle owns anything about the container in front of the user. Cloud owns\n> anything that is history, cross-instance, or account.**\n\nDozzle is optimized for the current view and can act on it; Cloud reviews everything\nat once. When a feature could live in either, ask which of those two questions it\nanswers.\n\n### Naming\n\nThe two Cloud surfaces must not blur into each other:\n\n- **Notifications** — the rules the user wrote, and what those rules did.\n- **Findings** — the rules the user never wrote, that Cloud noticed anyway.\n\nFindings are not alerts and are never listed alongside them.\n\n### What Cloud adds is memory, not features\n\nNothing Dozzle does locally today ever gets gated. Alerts already splice into the log\nstream (`AlertLogItem.vue`), they just die on refresh because nothing remembers them.\nCloud's contribution is that the same thing survives a reload, a restart, and a week —\nwhich is why an alert can come back later as a dot on a container row.\n\nSo a Cloud-less install shows an empty history section with one muted line saying what\nit would hold, not a locked card. A surface that has no local half at all (a findings\nlist) is mounted only when cloud is configured, rather than existing as a permanent\nupsell page in the nav.\n\n### Linking out\n\nLinking out to Cloud is fine, and expected, for anything Cloud genuinely does better:\nbilling and keys, the report archive, cross-instance rollups, deep-investigation\ntranscripts. `AlertHit.url` in `protos/cloud.proto` already carries the deep link.\n\nNever link out for something Dozzle could answer against the stream already on screen.\n\"Show me the lines\" — driving the historical scroll to the window a finding or alert\ndescribes — is the whole reason these surfaces live in Dozzle at all.\n\n### Gating\n\nPlan decisions are computed server-side in Cloud and handed to Dozzle already decided\n(`fixLocked`), never inferred from missing data — absence read as tier is how a paying\ncustomer gets shown an upsell for what they already bought. Dozzle renders the gate it\nis given and branches on nothing beyond `isPro` for cosmetics.\n\nOne upsell surface: `CloudPopover`. Everywhere else stays silent, per the design\nsystem's rule that `primary` is reserved for the single action a surface wants you to\ntake — a findings drawer wants \"Show me the lines\", not \"Upgrade\".\n\n## Design System\n\nThe UI is being converged on one visual language: clean, flat, and quiet. New or\nredesigned surfaces follow these rules; when touching an old surface, bring it along\nrather than matching what is already there.\n\n### Surfaces\n\n- A grouped surface is a **neutral panel**: `rounded-lg border border-base-content/15 bg-base-200/40`,\n  with rows separated by `divide-y divide-base-content/10`. Padding is `p-4` per row,\n  `p-2` for a row of link items. No shadows, no gradients, no `--depth`.\n- Floating surfaces (dropdowns, popovers, toasts) use `rounded-box border border-base-content/10 bg-base-200`\n  with `shadow-sm`/`shadow-lg`. `nav-menu-panel` in `main.css` is the sidebar's version.\n- Inside a panel, a hairline `<div class=\"bg-base-content/10 h-px\">` separates blocks when\n  `divide-y` does not apply (e.g. inside a dropdown's flow).\n- Interactive rows are `hover:bg-base-300 rounded-md px-2 py-1.5 transition-colors`, with a\n  leading icon at `size-4 opacity-60` and a trailing `mdi:open-in-new size-3.5 opacity-40`\n  when the row leaves the app.\n\n### Color and severity\n\n- **Severity rides on the icon, never on the surface.** A tinted circle\n  (`bg-error/10 text-error`, `bg-warning/10 text-warning`, `bg-info/10 text-info`) plus a\n  status dot carries the state; the panel behind it stays neutral so the text keeps full\n  contrast. Do not use daisyUI `alert alert-error` / `alert-warning` / `alert-success`:\n  a saturated block at drawer or page width shouts over everything near it.\n- Use `InlineNotice` (`assets/components/ui/InlineNotice.vue`) for an in-page notice and\n  `ToastModal` for a floating one. Both are neutral panels with a tinted glyph.\n- Status text is a `status-pill` (see `main.css`): a bordered, uppercase, mono chip in\n  `neutral`, `success`, `primary`, `secondary`, `warning` or `error`. Prefer it to `badge`.\n- `primary` is reserved for the single action a surface wants you to take. Two solid\n  primary buttons in one panel is a bug; the secondary one is a plain `btn`, and a\n  destructive secondary is `btn text-error`, not `btn-error`.\n\n### Typography and density\n\n- Body `text-sm`, secondary copy `text-base-content/60`, footnotes `text-xs` at\n  `text-base-content/40`. Drawer and page titles are `text-2xl font-bold` with a\n  `text-base-content/60` subtitle underneath.\n- Section headings inside a form are `FormStepHeading` (numbered chip + uppercase label);\n  standalone section headings are `text-base-content/60 font-semibold tracking-wide uppercase`.\n- Numbers, keys, IDs, periods, and percentages are `font-mono`. Emphasize the meaningful\n  half and mute the rest (`font-semibold` used, `text-base-content/40` for `/ limit`).\n- A read-only value is text, not a disabled input. Label left, mono value right.\n\n### Meters and charts\n\n- Usage meters are `UsageMeter` (`assets/components/ui/UsageMeter.vue`): a\n  `bg-base-content/10 h-1.5 rounded-full` track with a `bg-primary` fill that turns\n  `bg-warning` past 70% and `bg-error` past 90%. Do not use daisyUI `<progress>` for these;\n  it is taller than the type around it and carries its own palette.\n- Charts stay custom and lightweight (`BarChart.vue`); no chart library.\n\n### Affordances\n\n- Tailwind v4's preflight sets `cursor: default` on buttons, so a control reads as\n  painted on unless something puts the pointer back. One base-layer rule in `main.css`\n  does that for `button`, `summary`, `select`, `[role=\"button\"]`, `.btn` and\n  checkbox/radio labels, skipping disabled ones. Never add `cursor-pointer` to a new\n  button; if a clickable thing still shows an arrow, it is a `div` pretending to be a\n  button and the fix is to make it a `<button>`.\n- Anything clickable needs a hover state and a hit area a pointer can find. Under ~24px\n  the painted box is not the target: draw the hit area with an `::after` inset so\n  the layout around it does not move (`AlertDot.vue`).\n\n### Motion\n\n- Transitions are `transition-colors` on hover, `duration-500` on a meter's width, and\n  `200ms cubic-bezier(0.34, 1.56, 0.64, 1)` for the icon spring in `main.css`.\n- Icon-only buttons opt into the spring with `btn-circle`, `btn-square` or `icon-btn`; the\n  flavours (`icon-float`, `icon-spin`, `icon-wiggle`, `icon-ring`) are set on the glyph.\n- Every animation has a `prefers-reduced-motion` escape.\n\n### Mobile\n\n- Under `isMobile` the shell is a floating tab bar (`shell/MobileTabBar.vue`: Home, Browse,\n  Notifications, Settings, plus a separate search button). A log route is a pushed screen:\n  no tab bar, and `ScrollableView`'s header carries a back button. Which routes count as log\n  routes is `LOG_ROUTES` in `composable/app/mobileShell.ts`; a new log view goes there.\n- Floating chrome on a phone uses the `frosted` utility (translucent fill + backdrop blur),\n  which falls back to a solid `base-200` without `backdrop-filter` or under\n  `prefers-reduced-transparency`. It is a fill only; add the border and shadow yourself. Do\n  not use daisyUI's `glass`, which paints a diagonal sheen. Panels and rows stay flat.\n- A menu that is an action list opens as a bottom sheet on a phone: pass `sheet` to `Popover`.\n\n### Consistency rules\n\n- A panel keeps its shape across states. Loading, error and healthy branches of the same\n  surface share the header, dividers and footer so nothing reshuffles when state changes.\n- The same concept looks the same everywhere. The cloud account panel is the same parts at\n  three widths: `CloudPopover.vue` (compact), `CloudSettingsCard.vue`, and\n  `notifications/CloudDestinationForm.vue`. When a fourth surface needs it, extract a\n  component instead of copying the classes.\n- Do not repeat the container's title inside its own content: a drawer header already names\n  the thing, so the panel below leads with what the header cannot say.\n- Drawer footers are sticky, opaque and full-bleed, bled over the drawer's `p-6` and padded\n  clear of the home indicator:\n  `bg-base-100 border-base-content/10 sticky -bottom-6 z-10 -mx-6 -mb-6 mt-auto border-t px-10 pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]`\n  (widen `-mx`/`-mb` by any padding the occupant adds of its own).\n\n## Where files go\n\nFour tests for a new component, applied in order. The first that matches decides the\nfolder, so a contributor never has to ask.\n\n1. **`ui/`** — it imports nothing under `@/` except `@/composable/ui/`, **and** its name\n   carries no Dozzle noun (container, host, log, alert, cloud, nav, swarm, k8s, stack,\n   service, chat, rail). `ui/` means \"would still build pasted into a different Vue app\".\n   It does not mean \"shared\" and it does not mean \"small\": a `ui/` component with one\n   consumer is fine, and a large domain component with six consumers still lives in its\n   feature folder. Genericity is the test, popularity is not.\n2. **`cloud/`** — the surface does not exist at all in a cloud-less install. Two carve-outs,\n   settled once: anything `assets/models/LogEntry.ts` registers is a log entry and stays in\n   `logs/entries/` with its siblings, `AlertLogItem.vue` included, because alerts splice\n   into the local stream. And a variant that plugs into a local form stays with that form,\n   which is why `CloudDestinationForm.vue` sits beside `WebhookDestinationForm.vue`.\n3. **`views/`** — a file in `assets/pages/` mounts it as that route's whole body. One file\n   per route. (`ContainerLog.vue` is also mounted by `layouts/default.vue` for pinned\n   columns; it is still that route's view.)\n4. Otherwise the **feature folder** named by the noun in the component's own name.\n\n`nav/` is everything inside the sidebar; `shell/` is the frame that positions it. Nest one\nlevel inside a feature folder only past ~15 files and only with an obvious sub-noun, never\ntwo. A spec moves with its subject. `assets/composable/` uses the same folder names and the\nsame first two tests.\n\n`components/ui/` may depend on `composable/ui/` and third-party code, and nothing else.\nThat is the one dependency edge worth enforcing in review.\n\nMoving a component is cheap: `unplugin-vue-components` runs without `directoryAsNamespace`,\nso a component's name is its filename and nesting is invisible to every template. It also\nmeans **two components may never share a basename**, at any depth.\n\n## Important Development Notes\n\n### Frontend\n\n- Auto-imports are configured for Vue composables, components, and Pinia stores (see `vite.config.ts`)\n- Icons use unplugin-icons with multiple icon sets (mdi, carbon, material-symbols, etc.)\n- Tailwind CSS with DaisyUI for styling\n- TypeScript definitions auto-generated in `assets/auto-imports.d.ts` and `assets/components.d.ts`\n- **Log Entry Types**: Three types of log messages supported\n  - `SimpleLogEntry`: Single-line text logs (`string`)\n  - `ComplexLogEntry`: Structured JSON logs (`JSONObject`)\n  - `GroupedLogEntry`: Multi-line grouped logs (`string[]`)\n- **Type consistency**: Use `LogMessage` type alias instead of `string | string[] | JSONObject` for log entry messages\n- **Log Entry Factory Pattern**: Use `LogEntry.create(logEvent)` to instantiate the correct entry type based on `logEvent.t` field\n- **EventSource Buffering**: Log streams use buffer-based flushing (250ms debounce, 1000ms max) to batch UI updates\n- **Charts/Visualizations**: Custom lightweight implementations (no D3.js)\n  - `BarChart.vue`: Self-contained bar chart with responsive downsampling\n  - Downsampling algorithm: Averages data into buckets based on available screen width\n  - All stat history tracked in `Container.statsHistory` (max 300 items via rolling window)\n  - `chartData` is always a rolling window of max 300 items — array length stays constant\n  - Uses `ref` (not `computed`) for `downsampledBars` to enable in-place mutation of the last bar, avoiding full re-renders\n  - Component instance is reused when switching containers; after init the chart only patches the last bar per tick, so on a wholesale `chartData` replacement (container switch) the parent must call the exposed `recalculate()`. `MultiContainerStat` holds refs to its `BarChart`s and calls it in the `containers` watch. (Note: `Container` carries Vue `ref`s, so VueTestUtils `setProps` cannot retrigger such a watch — tests must swap the container via a parent `ref` re-render.)\n\n### Backend\n\n- The application uses Go 1.25+ with module support\n- Certificate generation is required (`make generate` creates shared_key.pem and shared_cert.pem)\n- Protocol buffer generation happens via `go generate` directive in `main.go`\n- Docker client uses API version negotiation for compatibility\n- **Service Layer Architecture**:\n  - `container.ClientService` interface abstracts Docker/K8s/Agent backends\n  - `hostservice.MultiHostService` orchestrates multi-host operations\n  - `ClientManager` implementations: `RetriableClientManager` (server mode), `SwarmClientManager` (swarm mode)\n- **Where backend code goes**: code that only one platform needs lives in `internal/container/<platform>/`;\n  code docker and k8s share lives in `internal/container/` (or `logparse/` for the log pipeline);\n  code that coordinates several hosts lives in `internal/hostservice/`. `internal/container` never\n  imports a platform package, and platforms never import each other.\n\n### Authentication\n\n- Three modes: none, simple (file-based users.yml), forward-proxy (e.g., Authelia)\n- JWT tokens for simple auth with configurable TTL\n- User file location: `./data/users.yml` or `./data/users.yaml`\n\n### Testing\n\n- Go tests use standard `testing` package with testify assertions\n- Frontend uses Vitest with `@vue/test-utils`\n- Integration tests with Playwright in `e2e/`\n- Tests must run with `TZ=UTC` for consistent timestamps\n\n### Container Stats & Metrics\n\n- Stats are tracked using exponential moving average (EMA) with alpha=0.2\n- History stored in rolling window (300 items max) via `useSimpleRefHistory`\n- CPU metrics normalized by core count (respects `cpuLimit` or falls back to host `nCPU`)\n- Memory metrics include both percentage and absolute usage (`memoryUsage` vs `memory`)\n- Stats visualization uses adaptive downsampling for performance\n\n### Container Labels\n\n- `dev.dozzle.name`: Custom container display name\n- `dev.dozzle.group`: Group containers together\n- `dev.dozzle.url`: Link a container to its own web UI (http/https only; `Container.url` in `assets/models/Container.ts`)\n- Label-based filtering throughout the application\n\n### Deployment Modes\n\n- **Server mode** (default): Single or multi-host Docker monitoring\n  - Uses `RetriableClientManager` with local + remote agent clients\n- **Swarm mode**: Automatic discovery of Swarm nodes via Docker API\n  - Creates gRPC agent server on each node (port 7007)\n  - Uses `SwarmClientManager` for node discovery\n- **K8s mode**: Pod log monitoring in Kubernetes cluster\n  - Implements `container.Client` interface via Kubernetes API\n- **Agent mode**: Lightweight gRPC agent for remote log collection\n  - Run with `dozzle agent` or `bun run agent:dev`\n  - Listens on port 7007 with TLS certificate authentication\n\n## Key Architectural Patterns\n\n### Backend Abstraction Layers\n\nThe backend follows a clean layered architecture:\n\n```\nHTTP Handlers (internal/web)\n    ↓\nHostService Interface (internal/hostservice)\n    ↓\ncontainer.ClientService Interface (per host)\n    ↓\ncontainer.Client Interface\n    ↓\nImplementation (internal/container/{docker,k8s,agent})\n```\n\n**When adding new container operations:**\n\n1. Define method in `container.Client` interface (`internal/container/client.go`)\n2. Implement in `internal/container/docker/client.go` (and `internal/container/k8s/client.go` if applicable)\n3. Add it to `container.ClientService` (`internal/container/client_service.go`) and implement it in each\n   platform's `service.go` (`docker/`, `k8s/`, `agent/`)\n4. Add HTTP handler in `internal/web/` with appropriate route\n\n### Frontend Data Flow\n\n**Real-time Log Viewing:**\n\n1. User navigates to `/container/{id}` route\n2. Page component calls `useContainerStream(container)` composable\n3. Composable creates EventSource connection to `/api/containers/logs/stream?container={host}/{id}` (repeated for a merged view)\n4. Backend streams `LogEvent` objects via SSE\n5. Frontend buffers events (250ms debounce, max 1000ms)\n6. Batched buffer flushes update reactive `messages` array\n7. `LogViewer.vue` renders using appropriate component (`SimpleLogItem`, `ComplexLogItem`, `GroupedLogItem`)\n8. When messages exceed `maxLogs` (400), oldest entries replaced or marked as `SkippedLogsEntry`\n\n**Stats Streaming:**\n\n1. `container.ts` store connects to `/api/events/stream` on app init\n2. Backend multiplexes container events and stats into single SSE stream\n3. `container-stat` events update `Container._stat` and append to `_statsHistory`\n4. EMA calculation provides smoothed `movingAverageStat` (alpha=0.2)\n5. `ContainerTable.vue` displays mini bar charts using `statsHistory` with downsampling\n\n### Protocol Buffer Flow (Agent Mode)\n\n1. Main server creates `agent.NewClient(endpoint, certs)` for each remote host\n2. AgentClient implements `container.Client` interface\n3. Method calls translate to gRPC requests defined in `protos/rpc.proto`\n4. Remote agent receives gRPC call, delegates to local `docker.Client`\n5. Streaming RPCs (logs, stats, events) use bidirectional channels\n6. Responses converted back to domain models in `internal/container/agent/convert.go`\n\n### Cloud Tool Execution Flow\n\n1. `cloud.Client.Run()` blocks until `Notify()` signals a cloud dispatcher is configured\n2. `connect()` establishes bidirectional gRPC stream (`ToolStream` RPC) to cloud endpoint\n3. Cloud sends `ToolRequest` (ListTools or CallTool), client dispatches via `executeTool()`\n4. Tool calls run concurrently (max 5 via weighted semaphore), responses sent back on stream\n5. On disconnect, exponential backoff (1s→30s with jitter) triggers reconnection\n6. `PermissionDenied` errors stop retrying permanently (invalid API key / no pro plan)\n7. Tool definitions cached via `sync.Once`; zero overhead for non-cloud users\n\n### Log Parsing Pipeline\n\n1. Docker API returns multiplexed stream (8-byte headers + payload)\n2. `container/docker/log_reader.go` parses headers, extracts stdout/stderr type\n3. `container/logparse/event_generator.go` receives raw log lines\n4. Detection logic identifies:\n   - JSON structure → `ComplexLogEntry`\n   - Multi-line patterns (stack traces) → `GroupedLogEntry`\n   - Single lines → `SimpleLogEntry`\n5. Log level extraction via regex patterns\n6. `LogEvent` serialized to JSON and sent via SSE\n7. Frontend deserializes and renders with appropriate component\n\n## Adding New Features\n\n### Adding a New HTTP Route\n\n1. Define route in `internal/web/routes.go` using chi router:\n   ```go\n   r.Get(\"/api/custom-endpoint\", h.customHandler)\n   ```\n2. Implement handler method in appropriate file (e.g., `actions.go`, `logs.go`)\n3. Use `hostService` to find container/host via `FindContainer()` or `FindHost()`\n4. Return JSON response or establish SSE/WebSocket stream\n\n### Adding a New Log View Type\n\n1. Create route file in `assets/pages/` (e.g., `custom/[id].vue`)\n2. Create composable in `assets/composable/logs/eventStreams.ts` (e.g., `useCustomStream()`)\n3. Composable should:\n   - Build API URL with appropriate filters\n   - Create EventSource connection\n   - Handle buffering and message batching\n   - Return reactive `messages` array and control methods\n4. Use `LogViewer.vue` component to render messages\n5. Add backend API endpoint if needed (see above)\n\n### Adding Container Stats/Metrics\n\n1. Add field to `Stat` type in `internal/container/types.go`\n2. Update `container/docker/stats_collector.go` (and `container/k8s/stats_collector.go`) to extract the metric\n3. Add calculation logic in `container/docker/calculation.go` if needed\n4. Ensure protobuf definition includes field in `protos/rpc.proto`, and map it in `container/agent/convert.go`\n5. Frontend automatically receives updates via existing SSE stream\n6. Update `Container` model in `assets/models/Container.ts` if UI needs access\n\n### Working with Notifications/Alerts\n\n**Backend** (`internal/notification/`):\n\n- `manager.go`: Rule evaluation engine, manages alert state\n- `log_listener.go`: Subscribes to container log streams, evaluates rules against incoming logs\n- `types.go`: Alert rule definitions (log pattern matching, thresholds)\n- `dispatcher/`: Notification channel implementations\n\n**Frontend** (`assets/pages/notifications.vue`, `assets/components/notifications/`):\n\n- `AlertForm.vue`, `DestinationForm.vue`: UI for creating rules\n- Rules persisted to `./data/notifications.yml` via `internal/notification/persist.go`\n- Alert state displayed in notification cards\n\n**Adding a new notification channel:**\n\n1. Implement dispatcher interface in `internal/notification/dispatcher/`\n2. Register in `manager.go` dispatcher factory\n3. Add UI form in `assets/components/notifications/DestinationForm.vue`\n\n### Adding a New Cloud Tool\n\n1. Define the tool in `AvailableTools()` in `internal/cloud/tools.go` with name, description, and parameter schema\n2. Add a response message type in `protos/cloud.proto` and add it to the `CallToolResponse.result` oneof\n3. Run `make generate` to regenerate protobuf code\n4. Add a case in the `executeTool()` switch in `internal/cloud/tools.go`\n5. Implement the execution function in the appropriate `tools_*.go` file\n6. Use `ToolHostService` interface methods to access container/host data\n7. Add tests in `tools_test.go`\n\n## Common Development Patterns\n\n### Testing\n\n- Always run Go tests with race detector: `go test -race`\n- Frontend tests require `TZ=UTC` for timestamp consistency\n- Integration tests use Playwright with `make int` (runs docker-compose setup)\n- Use `testify/assert` for Go test assertions\n\n### Hot Reload Development\n\n- `make dev` runs both backend (air) and frontend (vite) with hot reload\n- `DEV=true` disables embedded asset serving\n- `LIVE_FS=true` serves assets from filesystem instead of embedded\n- Backend changes trigger air restart automatically\n- Frontend changes trigger vite HMR\n\n### Debugging\n\n- Backend logs: Set `--level debug` flag or `DOZZLE_LEVEL=debug` env var\n- Frontend: Vue DevTools browser extension\n- SSE streams: Browser DevTools Network tab shows EventSource connections\n"},"items":[{"name":"SKILL.md","path":".claude/skills/update-int-snapshots/SKILL.md","rawUrl":"https://raw.githubusercontent.com/amir20/dozzle/HEAD/.claude/skills/update-int-snapshots/SKILL.md","title":"update-int-snapshots","category":"anthropic-skill","format":"markdown","content":"---\nname: update-int-snapshots\ndescription: Use when Playwright visual snapshots in e2e/visual.spec.ts-snapshots/ need to be regenerated after a UI change (logo, theme, layout, anything that affects rendered pixels) and the integration suite needs to be rerun via make int\n---\n\n# Update Integration Snapshots\n\n## When to Use\n\n- A visual change landed (logo, color, spacing, fonts) and `make int` will fail on `visual.spec.ts` until snapshots are regenerated.\n- A snapshot test reports diffs you've confirmed are intentional.\n\nDo NOT use when diffs are unintentional regressions — investigate first.\n\n## Procedure\n\n1. **Delete the stale snapshots** so Playwright writes fresh ones (don't try to update in place, the old PNGs can confuse the diff):\n\n   ```bash\n   rm e2e/visual.spec.ts-snapshots/*.png\n   ```\n\n2. **Run with `--update-snapshots` via `compose up`.** You must use `compose up` (not `compose run`) because the navigation sidebar snapshot includes the live container list, and the two modes produce different sibling containers. The cleanest way is to patch the playwright command in `docker-compose.yml` temporarily:\n\n   ```bash\n   sed -i.bak 's|command: npx --yes playwright test|command: npx --yes playwright test --update-snapshots|' docker-compose.yml\n   make int\n   mv docker-compose.yml.bak docker-compose.yml\n   ```\n\n   Snapshots must be generated in Linux/Chromium (the compose image), not macOS, because filenames include the platform suffix (e.g. `-chromium-linux.png`).\n\n   Nothing needs clearing first. The compose file publishes no host ports, every container is named `dozzle_e2e_*` under the fixed project `dozzle_e2e`, and filters match those names exactly. Other containers on the machine can't block the run or show up in the snapshots, and a leftover run from another worktree is recreated. Two worktrees running `make int` at the same time do still clobber each other, so run one at a time.\n\n3. **Verify** by rerunning the normal suite:\n\n   ```bash\n   make int\n   ```\n\n   Should now pass cleanly.\n\n4. **Commit the regenerated PNGs** alongside the UI change so CI stays green.\n\n## Common Mistakes\n\n- **Running `npx playwright test --update-snapshots` locally on macOS.** Generates `-darwin` filenames CI doesn't use. Always go through the docker compose image.\n- **Forgetting to delete first.** `--update-snapshots` does overwrite, but if the test layout changed (new test, renamed snapshot), stale PNGs are left behind. Wipe-and-regenerate is safest.\n- **Adding a test container without the prefix, or a filter without anchors.** `name=dozzle` is a substring match and pulls in any local container with \"dozzle\" in its name. Use `container_name: dozzle_e2e_<service>` and `DOZZLE_FILTER=name=^dozzle_e2e_<service>$$` (`$$` escapes `$` in compose).\n- **Publishing a host port \"for debugging\".** It brings back conflicts with whatever else is on the machine. Reach instances by service name from inside the network.\n\n## Quick Reference\n\n```bash\nrm e2e/visual.spec.ts-snapshots/*.png\nsed -i.bak 's|command: npx --yes playwright test|& --update-snapshots|' docker-compose.yml\nmake int\nmv docker-compose.yml.bak docker-compose.yml\nmake int   # verify pass\n```\n","frontmatter":{"name":"update-int-snapshots","description":"Use when Playwright visual snapshots in e2e/visual.spec.ts-snapshots/ need to be regenerated after a UI change (logo, theme, layout, anything that affects rendered pixels) and the integration suite needs to be rerun via make int"},"isInternal":false,"tokens":777,"sizeBytes":3188},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/amir20/dozzle/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Response Style\n\nKeep chat responses brief: bullet points, `file:line` references, no \"Strengths\" or\n\"Summary\" sections. What the PR review bot looks for is set in\n`.github/workflows/claude-code-review.yml`, not here.\n\n## Translations\n\n`locales/en.yml` is the source of truth, but it is never the only file to change. Whenever you add, rename, or remove a key in `en.yml`, apply the same change to **every** other file in `locales/` in the same commit, with the string actually translated (not the English text copied over). Keys are also deleted everywhere when they lose their last consumer.\n\nThere is no Crowdin or Weblate sync in this repo, so a key that only exists in `en.yml` silently falls back to English for every other locale and nothing ever flags it.\n\n### Docs Translations\n\n`docs/` is translated into `zh`, `de`, `fr` and `es`. The English page under `docs/` is the source of truth, and `docs/<locale>/<same path>` mirrors it. Changing an English page means updating all four translations in the same commit.\n\nUnlike `locales/`, there is no fallback here. A stale translated page renders confidently wrong instructions rather than quietly showing English, so drift is worse than a missing key.\n\nEach translated file carries a `sourceHash` in its frontmatter recording the English source it was written from. `bun docs/scripts/check-translations.mjs` fails when they diverge, and runs in CI as the `Docs Translations` job. After actually translating the changed prose, re-stamp with:\n\n```bash\nbun docs/scripts/check-translations.mjs --update\n```\n\n`--update` only re-stamps hashes. It does not translate anything, so running it on an untranslated page turns CI green while leaving the page wrong. Translate first.\n\nInside translated pages, everything in a fenced code block stays byte-identical to English except natural-language comments, and internal links are locale-prefixed (`/guide/agent` becomes `/de/guide/agent`). The sidebar is not duplicated per locale: its structure lives in `docs/.vitepress/locales/structure.ts` and each locale file supplies only labels, so a new page needs one slug there plus one label per locale.\n\n## Testing Unreleased PRs\n\nWhen replying to a GitHub issue or discussion where the fix lives in an open PR, ask the reporter to test the pre-built image: `amir20/dozzle:pr-XXX` (XXX = PR number). CI builds a tagged image per PR, so reporters can verify without waiting for the next release.\n\n## GitHub Tone (issues, PRs, comments, discussions)\n\nWhen posting anything to GitHub, write like a human maintainer, not an AI assistant. Avoid telltale LLM patterns:\n\n- No em dashes or en dashes. Use commas, periods, or parentheses instead.\n- No \"Not X, but Y\" rhetorical contrasts.\n- No throat-clearing openers (\"Great point\", \"Makes sense\", \"Thanks for the detailed write-up\").\n- No closing summaries or recap sentences.\n- No bolded inline labels mid-paragraph (\"**Why:**\", \"**Note:**\").\n- Drop hedges (\"essentially\", \"basically\", \"essentially just\"). Say it plain.\n- Lowercase casual tone is fine. Contractions are fine. Short sentences are fine.\n- Don't over-explain tradeoffs. State the decision, give one reason, stop.\n\n## Scope Discipline\n\nSmall first versions win. Building is cheap here; reviewing and maintaining is not.\n\n- **Agree the scope as a changelog before any code.** For a new feature, write what\n  ships (user-visible bullets) and an explicit \"Not in this release\" list, and get it\n  approved. Anything not on the ship list is out of scope, however reasonable it looks.\n- **A plan is a menu, not a work order.** Build phase 1 only. Later phases start after\n  phase 1 has merged, unless the user asks for more.\n- **Reuse before you add.** A new table, RPC, proto field, flag, label or setting needs a\n  one-line reason why an existing one (labels, existing events, existing alert channels,\n  existing pages) can't do it. Prefer one delivery surface to several.\n- **Stop at ~1,000 lines.** If a PR's non-test, non-generated diff passes ~1,000 lines,\n  stop and check scope with the user before continuing.\n- **One feature, one PR per repo.** Unrelated work (e.g. a settings redesign during an\n  updates feature) gets its own PR off main, never a layer in the same stack. Avoid\n  stacks deeper than two; with a GitHub stack, merge from the bottom.\n- **Subagents and workflows get the out-of-scope list.** Every implementation prompt\n  includes \"not in scope\" and the rule: a deviation that adds behaviour is reported,\n  not built.\n- **Review fixes fix; they don't extend.** Addressing feedback must not add new\n  mechanisms. If a fix needs one, raise it as a scope question.\n- **Audit size before review, not after.** When a body of work is done, report its size\n  per feature (code, tests, generated) next to its user impact, so cuts happen before\n  review time is spent.\n\n## Project Overview\n\nDozzle is a lightweight, web-based Docker log viewer with real-time monitoring capabilities. It's a hybrid application with:\n\n- **Backend**: Go (HTTP server, Docker API client, WebSocket streaming)\n- **Frontend**: Vue 3 (SPA with Vite, TypeScript)\n\nThe application supports multiple deployment modes: standalone server, Docker Swarm, and Kubernetes (k8s).\n\n## Development Commands\n\n### Setup\n\n```bash\n# Install dependencies\nbun install\n\n# Generate certificates and protobuf files\nmake generate\n```\n\n### Development\n\n```bash\n# Run full development environment (backend + frontend with hot reload)\nmake dev\n\n# Same, on a free port trio derived from this checkout's path, so several worktrees\n# can each run an instance at once. Prints the URL it picked.\nmake dev-auto\n\n# Alternative: Run backend and frontend separately\nbun run watch:backend  # Go backend with air (port 3100)\nbun run watch:frontend # Vite dev server (port 3100)\n\n# Run in agent mode for development\nbun run agent:dev\n```\n\n### Building\n\n```bash\n# Build frontend assets\nbun run build\n# or\nmake dist\n\n# Build entire application (includes frontend build)\nmake build\n\n# Build Docker image\nmake docker\n```\n\n### Testing\n\n```bash\n# Run Go tests\nmake test\n\n# Run frontend tests (Vitest)\nbun run test\n# Run in watch mode\nTZ=UTC bun run test --watch\n\n# Type checking\nbun run typecheck\n\n# Lint (Tailwind classes only)\nbun run lint\nbun run lint --fix\n```\n\nAlways go through `bun run` for `test` and `build`. `bun test` and `bun build` are\nbun's own test runner and bundler, not the scripts in `package.json`, and they fail\nin confusing ways against this codebase.\n\n`bun run` does not mean the bun runtime: it follows each tool's `#!/usr/bin/env node`\nshebang unless the script says `bun --bun`. Every script in `package.json` that starts a\nJS tool carries that flag except `test` and `typecheck`, which need node: under `--bun` vitest's jsdom\nworkers fail to start and `vue-tsc` reports a false TS2614. `release` (bumpp) is\nuntested on bun and also left alone. Those are the only reasons node is still required.\n\n### Linting\n\nESLint here checks Tailwind classes and nothing else. It is not a general JS or TS\nlinter, and it should not become one: `tsconfig.json` already has `strict`,\n`strictNullChecks` and `noUnusedLocals`, so `vue-tsc` covers that ground, and prettier\nplus `prettier-plugin-tailwindcss` own formatting and class order. No stylistic rule\nbelongs in `eslint.config.js`, which is also why `eslint-config-prettier` is not needed.\n\nEvery rule the Tailwind plugin ships is listed in that config with the reason it is on\nor off, so add to that list rather than reaching for a preset.\n\n`eslint --fix` runs on staged `js`/`mjs`/`ts`/`mts`/`vue` in the pre-commit hook, before\nprettier so prettier gets the last word on formatting. Run `bun run lint --fix` by hand\nafter writing a batch of markup if you would rather not find out at commit time.\n\nTwo blind spots worth knowing, because a green run does not mean a clean tree:\n\n- `@apply` inside an SFC `<style>` block is never handed to the plugin, so a bad class\n  there is never reported.\n- The arbitrary-value rule only maps whole-number spacing steps. `gap-[2px]` and `w-[3px]`\n  go unreported even though `gap-0.5` and `w-0.75` exist.\n\n### Preview & Other\n\n```bash\n# Preview production build locally\nbun run preview\n# or\nmake preview\n\n# Run integration tests (Playwright)\nmake int\n```\n\n## Architecture\n\n### Backend (Go)\n\nThe Go backend is organized into these key packages:\n\n- **`internal/web/`** - HTTP server and routing layer\n  - Routes defined in `routes.go` using chi router\n  - WebSocket/SSE handlers for log streaming (`logs.go`)\n  - Authentication middleware and token management (`auth.go`)\n  - Container action handlers (`actions.go`)\n  - `sse/`: the SSE writer; `search/`: log filtering, highlighting and HTML escaping\n\n- **`internal/container/`** - The generic layer every platform implements\n  - `types.go`, `host.go`: domain models (`Container`, `Host`, `LogEvent`, `ContainerStat`)\n  - `client.go`: `container.Client`, the raw engine interface\n  - `client_service.go`: `container.ClientService`, the per-host contract the rest of the app uses\n  - `store.go`: the container cache docker and k8s share (its header comment explains who\n    may write to the map), split by concern into `store_events.go` (the event loop),\n    `store_refresh.go` (reconnect, list and reconcile) and `store_fanout.go`\n    (bounded delivery to subscribers)\n  - `collector_lifecycle.go`: the lazy start/idle-stop reference count both stats collectors use\n  - `logparse/`: the log pipeline both platforms feed (`event_generator.go` grouping and JSON\n    detection, `level_guesser.go`, `logfmt.go`, `timestamp_prefix.go`)\n  - One folder per platform, each holding that platform end to end:\n    - `docker/`: `client.go`, `log_reader.go`, `stats_collector.go`, and `service.go` (`docker.Service`)\n    - `k8s/`: `client.go`, `log_reader.go`, `stats_collector.go`, and `service.go` (`k8s.Service`)\n    - `agent/`: gRPC `client.go`/`server.go`, `convert.go` (proto conversion), `service.go`, and\n      generated `pb/` (protos in `protos/`)\n\n- **`internal/hostservice/`** - Orchestration across hosts, one per deployment mode\n  - `multi_host.go`: `MultiHostService` (server and swarm modes)\n  - `retriable_client_manager.go` (server), `swarm_client_manager.go` (swarm)\n  - `k8s_cluster.go`: `K8sClusterService` (k8s mode)\n\n- **`internal/cli/`** - Command-line parsing, subcommands, client wiring (`clients.go`)\n\n- **`internal/cloud/`** - Dozzle Cloud integration (tool execution engine)\n  - `client.go`: Bidirectional gRPC stream client with auto-reconnect and exponential backoff\n  - `tools.go`: Tool registration, dispatch (`executeTool`), and `ToolHostService` interface\n  - `tools_containers.go`: Container listing, finding, stats, and inspection tools\n  - `tools_logs.go`: Log fetching with level/query/regex filtering (max 100 lines)\n  - `tools_actions.go`: Container start/stop/restart actions (gated by `enableActions`)\n  - `tools_helpers.go`: Proto conversion utilities and host name resolution\n  - Uses `protos/cloud.proto` for service and message definitions\n\n- **`internal/auth/`** - Authentication providers\n  - Simple file-based auth (`simple.go`)\n  - Forward proxy auth (`proxy.go`)\n  - Role-based authorization (`roles.go`)\n\n- **`internal/notification/`** - Alert and notification system\n  - `manager.go`: Notification rule evaluation and dispatching\n  - `log_listener.go`: Log pattern matching for alerts\n  - `dispatcher/`: Notification channel implementations (email, webhook, etc.)\n\n- **`main.go`** - Application entry point with mode switching (server/swarm/k8s/agent)\n\n### Frontend (Vue 3)\n\nThe frontend uses file-based routing with these conventions:\n\n- **`assets/pages/`** - File-based routes (unplugin-vue-router)\n  - `container/[id].vue`: Single container view\n  - `merged/[ids].vue`: Multi-container merged view\n  - `host/[id].vue`: Host-level logs\n  - `service/[name].vue`: Swarm service logs\n  - `stack/[name].vue`: Docker stack logs\n  - `group/[name].vue`: Custom grouped logs\n\n- **`assets/components/`** - Vue components (auto-imported). See \"Where files go\" below.\n  - `ui/`: generic primitives (`BarChart.vue`, `MetricCard.vue`, `Popover.vue`, `UsageMeter.vue`, ...)\n  - `shell/`: app frame and the singletons `layouts/default.vue` mounts once\n  - `nav/`: everything inside the sidebar\n  - `search/`: the command palette and search surfaces\n  - `containers/`: `ContainerTable.vue`, the toolbar, the terminal\n  - `hosts/`: `HostCard.vue` and friends\n  - `logs/`: the stream, with `logs/entries/` holding one file per `LogEntry` type\n    (`SimpleLogItem.vue`, `ComplexLogItem.vue`, `GroupedLogItem.vue`,\n    `ContainerEventLogItem.vue`, `SkippedEntriesLogItem.vue`, `LoadMoreLogItem.vue`)\n  - `views/`: one file per route that renders a log view, mounted by `assets/pages/`\n  - `notifications/`: the rules the user wrote\n  - `cloud/`: memory, cross-instance and account, split into `rail/`, `chat/`, `history/`\n\n- **`assets/stores/`** - Pinia stores (auto-imported)\n  - `config.ts`: App configuration and feature flags (injected from backend HTML, frozen immutable)\n  - `container.ts`: Container state management with EventSource streaming (`/api/events/stream`)\n  - `hosts.ts`: Multi-host state\n  - `settings.ts`: User preferences (localStorage-backed via profileStorage)\n  - `pinned.ts`: Pinned container logs for side-by-side viewing\n  - `swarm.ts`, `k8s.ts`: Deployment mode-specific state\n  - `announcements.ts`: Feature announcements\n\n- **`assets/composable/`** - Vue composables (auto-imported), foldered to mirror the components\n  - `ui/`: `popover.ts`, `media.ts`, `timeTicker.ts` - no domain knowledge\n  - `app/`: shell and global session state (`drawer.ts`, `theme.ts`, `toast.ts`, ...)\n  - `logs/`: the stream pipeline (`eventStreams.ts` SSE with 250ms buffered flushing,\n    `historicalLogs.ts`, `logContext.ts` provide/inject, `scrollContext.ts`,\n    `visible.ts`, `duckdb.ts` for SQL over logs)\n  - `containers/`: `containerActions.ts`, `imageUpdate.ts`, ...\n  - `cloud/`, `notifications/`, `editor/`\n\n  The `AutoImport` `dirs` glob is `assets/composable/**`. It must stay recursive: a bare\n  directory is scanned one level deep, and nested composables silently stop being\n  auto-imported with no error. The `**` also globs `*.spec.ts`, which is harmless only\n  while no spec exports anything.\n\n- **`assets/modules/`** - Vue plugins\n  - `router.ts`: Vue Router configuration\n  - `pinia.ts`: Pinia store setup\n  - `i18n.ts`: Internationalization\n\n### Communication Flow\n\n1. **Real-time Logs**: Frontend establishes SSE connections to `/api/containers/logs/stream?container={host}/{id}` (repeated for a merged view)\n2. **Container Events**: SSE stream at `/api/events/stream` pushes container lifecycle events\n3. **Stats**: Real-time CPU/memory stats streamed via SSE alongside events\n4. **Actions**: POST to `/api/hosts/{host}/containers/{id}/actions/{action}` (start/stop/restart)\n5. **Terminal**: WebSocket connections for container attach/exec at `/api/hosts/{host}/containers/{id}/attach`\n\n### Build System\n\n- **Frontend**: Vite builds to `dist/` with manifest\n- **Backend**: Embeds `dist/` using Go embed directive\n- **Hot Reload**: In development, `DEV=true` disables embedded assets, `LIVE_FS=true` serves from filesystem\n- **Makefile**: Orchestrates builds and dependency generation\n\n## Dozzle and Dozzle Cloud\n\nCloud features are folded into the Dozzle UI rather than linked to. The split that\ndecides where a feature lands:\n\n> **Dozzle owns anything about the container in front of the user. Cloud owns\n> anything that is history, cross-instance, or account.**\n\nDozzle is optimized for the current view and can act on it; Cloud reviews everything\nat once. When a feature could live in either, ask which of those two questions it\nanswers.\n\n### Naming\n\nThe two Cloud surfaces must not blur into each other:\n\n- **Notifications** — the rules the user wrote, and what those rules did.\n- **Findings** — the rules the user never wrote, that Cloud noticed anyway.\n\nFindings are not alerts and are never listed alongside them.\n\n### What Cloud adds is memory, not features\n\nNothing Dozzle does locally today ever gets gated. Alerts already splice into the log\nstream (`AlertLogItem.vue`), they just die on refresh because nothing remembers them.\nCloud's contribution is that the same thing survives a reload, a restart, and a week —\nwhich is why an alert can come back later as a dot on a container row.\n\nSo a Cloud-less install shows an empty history section with one muted line saying what\nit would hold, not a locked card. A surface that has no local half at all (a findings\nlist) is mounted only when cloud is configured, rather than existing as a permanent\nupsell page in the nav.\n\n### Linking out\n\nLinking out to Cloud is fine, and expected, for anything Cloud genuinely does better:\nbilling and keys, the report archive, cross-instance rollups, deep-investigation\ntranscripts. `AlertHit.url` in `protos/cloud.proto` already carries the deep link.\n\nNever link out for something Dozzle could answer against the stream already on screen.\n\"Show me the lines\" — driving the historical scroll to the window a finding or alert\ndescribes — is the whole reason these surfaces live in Dozzle at all.\n\n### Gating\n\nPlan decisions are computed server-side in Cloud and handed to Dozzle already decided\n(`fixLocked`), never inferred from missing data — absence read as tier is how a paying\ncustomer gets shown an upsell for what they already bought. Dozzle renders the gate it\nis given and branches on nothing beyond `isPro` for cosmetics.\n\nOne upsell surface: `CloudPopover`. Everywhere else stays silent, per the design\nsystem's rule that `primary` is reserved for the single action a surface wants you to\ntake — a findings drawer wants \"Show me the lines\", not \"Upgrade\".\n\n## Design System\n\nThe UI is being converged on one visual language: clean, flat, and quiet. New or\nredesigned surfaces follow these rules; when touching an old surface, bring it along\nrather than matching what is already there.\n\n### Surfaces\n\n- A grouped surface is a **neutral panel**: `rounded-lg border border-base-content/15 bg-base-200/40`,\n  with rows separated by `divide-y divide-base-content/10`. Padding is `p-4` per row,\n  `p-2` for a row of link items. No shadows, no gradients, no `--depth`.\n- Floating surfaces (dropdowns, popovers, toasts) use `rounded-box border border-base-content/10 bg-base-200`\n  with `shadow-sm`/`shadow-lg`. `nav-menu-panel` in `main.css` is the sidebar's version.\n- Inside a panel, a hairline `<div class=\"bg-base-content/10 h-px\">` separates blocks when\n  `divide-y` does not apply (e.g. inside a dropdown's flow).\n- Interactive rows are `hover:bg-base-300 rounded-md px-2 py-1.5 transition-colors`, with a\n  leading icon at `size-4 opacity-60` and a trailing `mdi:open-in-new size-3.5 opacity-40`\n  when the row leaves the app.\n\n### Color and severity\n\n- **Severity rides on the icon, never on the surface.** A tinted circle\n  (`bg-error/10 text-error`, `bg-warning/10 text-warning`, `bg-info/10 text-info`) plus a\n  status dot carries the state; the panel behind it stays neutral so the text keeps full\n  contrast. Do not use daisyUI `alert alert-error` / `alert-warning` / `alert-success`:\n  a saturated block at drawer or page width shouts over everything near it.\n- Use `InlineNotice` (`assets/components/ui/InlineNotice.vue`) for an in-page notice and\n  `ToastModal` for a floating one. Both are neutral panels with a tinted glyph.\n- Status text is a `status-pill` (see `main.css`): a bordered, uppercase, mono chip in\n  `neutral`, `success`, `primary`, `secondary`, `warning` or `error`. Prefer it to `badge`.\n- `primary` is reserved for the single action a surface wants you to take. Two solid\n  primary buttons in one panel is a bug; the secondary one is a plain `btn`, and a\n  destructive secondary is `btn text-error`, not `btn-error`.\n\n### Typography and density\n\n- Body `text-sm`, secondary copy `text-base-content/60`, footnotes `text-xs` at\n  `text-base-content/40`. Drawer and page titles are `text-2xl font-bold` with a\n  `text-base-content/60` subtitle underneath.\n- Section headings inside a form are `FormStepHeading` (numbered chip + uppercase label);\n  standalone section headings are `text-base-content/60 font-semibold tracking-wide uppercase`.\n- Numbers, keys, IDs, periods, and percentages are `font-mono`. Emphasize the meaningful\n  half and mute the rest (`font-semibold` used, `text-base-content/40` for `/ limit`).\n- A read-only value is text, not a disabled input. Label left, mono value right.\n\n### Meters and charts\n\n- Usage meters are `UsageMeter` (`assets/components/ui/UsageMeter.vue`): a\n  `bg-base-content/10 h-1.5 rounded-full` track with a `bg-primary` fill that turns\n  `bg-warning` past 70% and `bg-error` past 90%. Do not use daisyUI `<progress>` for these;\n  it is taller than the type around it and carries its own palette.\n- Charts stay custom and lightweight (`BarChart.vue`); no chart library.\n\n### Affordances\n\n- Tailwind v4's preflight sets `cursor: default` on buttons, so a control reads as\n  painted on unless something puts the pointer back. One base-layer rule in `main.css`\n  does that for `button`, `summary`, `select`, `[role=\"button\"]`, `.btn` and\n  checkbox/radio labels, skipping disabled ones. Never add `cursor-pointer` to a new\n  button; if a clickable thing still shows an arrow, it is a `div` pretending to be a\n  button and the fix is to make it a `<button>`.\n- Anything clickable needs a hover state and a hit area a pointer can find. Under ~24px\n  the painted box is not the target: draw the hit area with an `::after` inset so\n  the layout around it does not move (`AlertDot.vue`).\n\n### Motion\n\n- Transitions are `transition-colors` on hover, `duration-500` on a meter's width, and\n  `200ms cubic-bezier(0.34, 1.56, 0.64, 1)` for the icon spring in `main.css`.\n- Icon-only buttons opt into the spring with `btn-circle`, `btn-square` or `icon-btn`; the\n  flavours (`icon-float`, `icon-spin`, `icon-wiggle`, `icon-ring`) are set on the glyph.\n- Every animation has a `prefers-reduced-motion` escape.\n\n### Mobile\n\n- Under `isMobile` the shell is a floating tab bar (`shell/MobileTabBar.vue`: Home, Browse,\n  Notifications, Settings, plus a separate search button). A log route is a pushed screen:\n  no tab bar, and `ScrollableView`'s header carries a back button. Which routes count as log\n  routes is `LOG_ROUTES` in `composable/app/mobileShell.ts`; a new log view goes there.\n- Floating chrome on a phone uses the `frosted` utility (translucent fill + backdrop blur),\n  which falls back to a solid `base-200` without `backdrop-filter` or under\n  `prefers-reduced-transparency`. It is a fill only; add the border and shadow yourself. Do\n  not use daisyUI's `glass`, which paints a diagonal sheen. Panels and rows stay flat.\n- A menu that is an action list opens as a bottom sheet on a phone: pass `sheet` to `Popover`.\n\n### Consistency rules\n\n- A panel keeps its shape across states. Loading, error and healthy branches of the same\n  surface share the header, dividers and footer so nothing reshuffles when state changes.\n- The same concept looks the same everywhere. The cloud account panel is the same parts at\n  three widths: `CloudPopover.vue` (compact), `CloudSettingsCard.vue`, and\n  `notifications/CloudDestinationForm.vue`. When a fourth surface needs it, extract a\n  component instead of copying the classes.\n- Do not repeat the container's title inside its own content: a drawer header already names\n  the thing, so the panel below leads with what the header cannot say.\n- Drawer footers are sticky, opaque and full-bleed, bled over the drawer's `p-6` and padded\n  clear of the home indicator:\n  `bg-base-100 border-base-content/10 sticky -bottom-6 z-10 -mx-6 -mb-6 mt-auto border-t px-10 pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]`\n  (widen `-mx`/`-mb` by any padding the occupant adds of its own).\n\n## Where files go\n\nFour tests for a new component, applied in order. The first that matches decides the\nfolder, so a contributor never has to ask.\n\n1. **`ui/`** — it imports nothing under `@/` except `@/composable/ui/`, **and** its name\n   carries no Dozzle noun (container, host, log, alert, cloud, nav, swarm, k8s, stack,\n   service, chat, rail). `ui/` means \"would still build pasted into a different Vue app\".\n   It does not mean \"shared\" and it does not mean \"small\": a `ui/` component with one\n   consumer is fine, and a large domain component with six consumers still lives in its\n   feature folder. Genericity is the test, popularity is not.\n2. **`cloud/`** — the surface does not exist at all in a cloud-less install. Two carve-outs,\n   settled once: anything `assets/models/LogEntry.ts` registers is a log entry and stays in\n   `logs/entries/` with its siblings, `AlertLogItem.vue` included, because alerts splice\n   into the local stream. And a variant that plugs into a local form stays with that form,\n   which is why `CloudDestinationForm.vue` sits beside `WebhookDestinationForm.vue`.\n3. **`views/`** — a file in `assets/pages/` mounts it as that route's whole body. One file\n   per route. (`ContainerLog.vue` is also mounted by `layouts/default.vue` for pinned\n   columns; it is still that route's view.)\n4. Otherwise the **feature folder** named by the noun in the component's own name.\n\n`nav/` is everything inside the sidebar; `shell/` is the frame that positions it. Nest one\nlevel inside a feature folder only past ~15 files and only with an obvious sub-noun, never\ntwo. A spec moves with its subject. `assets/composable/` uses the same folder names and the\nsame first two tests.\n\n`components/ui/` may depend on `composable/ui/` and third-party code, and nothing else.\nThat is the one dependency edge worth enforcing in review.\n\nMoving a component is cheap: `unplugin-vue-components` runs without `directoryAsNamespace`,\nso a component's name is its filename and nesting is invisible to every template. It also\nmeans **two components may never share a basename**, at any depth.\n\n## Important Development Notes\n\n### Frontend\n\n- Auto-imports are configured for Vue composables, components, and Pinia stores (see `vite.config.ts`)\n- Icons use unplugin-icons with multiple icon sets (mdi, carbon, material-symbols, etc.)\n- Tailwind CSS with DaisyUI for styling\n- TypeScript definitions auto-generated in `assets/auto-imports.d.ts` and `assets/components.d.ts`\n- **Log Entry Types**: Three types of log messages supported\n  - `SimpleLogEntry`: Single-line text logs (`string`)\n  - `ComplexLogEntry`: Structured JSON logs (`JSONObject`)\n  - `GroupedLogEntry`: Multi-line grouped logs (`string[]`)\n- **Type consistency**: Use `LogMessage` type alias instead of `string | string[] | JSONObject` for log entry messages\n- **Log Entry Factory Pattern**: Use `LogEntry.create(logEvent)` to instantiate the correct entry type based on `logEvent.t` field\n- **EventSource Buffering**: Log streams use buffer-based flushing (250ms debounce, 1000ms max) to batch UI updates\n- **Charts/Visualizations**: Custom lightweight implementations (no D3.js)\n  - `BarChart.vue`: Self-contained bar chart with responsive downsampling\n  - Downsampling algorithm: Averages data into buckets based on available screen width\n  - All stat history tracked in `Container.statsHistory` (max 300 items via rolling window)\n  - `chartData` is always a rolling window of max 300 items — array length stays constant\n  - Uses `ref` (not `computed`) for `downsampledBars` to enable in-place mutation of the last bar, avoiding full re-renders\n  - Component instance is reused when switching containers; after init the chart only patches the last bar per tick, so on a wholesale `chartData` replacement (container switch) the parent must call the exposed `recalculate()`. `MultiContainerStat` holds refs to its `BarChart`s and calls it in the `containers` watch. (Note: `Container` carries Vue `ref`s, so VueTestUtils `setProps` cannot retrigger such a watch — tests must swap the container via a parent `ref` re-render.)\n\n### Backend\n\n- The application uses Go 1.25+ with module support\n- Certificate generation is required (`make generate` creates shared_key.pem and shared_cert.pem)\n- Protocol buffer generation happens via `go generate` directive in `main.go`\n- Docker client uses API version negotiation for compatibility\n- **Service Layer Architecture**:\n  - `container.ClientService` interface abstracts Docker/K8s/Agent backends\n  - `hostservice.MultiHostService` orchestrates multi-host operations\n  - `ClientManager` implementations: `RetriableClientManager` (server mode), `SwarmClientManager` (swarm mode)\n- **Where backend code goes**: code that only one platform needs lives in `internal/container/<platform>/`;\n  code docker and k8s share lives in `internal/container/` (or `logparse/` for the log pipeline);\n  code that coordinates several hosts lives in `internal/hostservice/`. `internal/container` never\n  imports a platform package, and platforms never import each other.\n\n### Authentication\n\n- Three modes: none, simple (file-based users.yml), forward-proxy (e.g., Authelia)\n- JWT tokens for simple auth with configurable TTL\n- User file location: `./data/users.yml` or `./data/users.yaml`\n\n### Testing\n\n- Go tests use standard `testing` package with testify assertions\n- Frontend uses Vitest with `@vue/test-utils`\n- Integration tests with Playwright in `e2e/`\n- Tests must run with `TZ=UTC` for consistent timestamps\n\n### Container Stats & Metrics\n\n- Stats are tracked using exponential moving average (EMA) with alpha=0.2\n- History stored in rolling window (300 items max) via `useSimpleRefHistory`\n- CPU metrics normalized by core count (respects `cpuLimit` or falls back to host `nCPU`)\n- Memory metrics include both percentage and absolute usage (`memoryUsage` vs `memory`)\n- Stats visualization uses adaptive downsampling for performance\n\n### Container Labels\n\n- `dev.dozzle.name`: Custom container display name\n- `dev.dozzle.group`: Group containers together\n- `dev.dozzle.url`: Link a container to its own web UI (http/https only; `Container.url` in `assets/models/Container.ts`)\n- Label-based filtering throughout the application\n\n### Deployment Modes\n\n- **Server mode** (default): Single or multi-host Docker monitoring\n  - Uses `RetriableClientManager` with local + remote agent clients\n- **Swarm mode**: Automatic discovery of Swarm nodes via Docker API\n  - Creates gRPC agent server on each node (port 7007)\n  - Uses `SwarmClientManager` for node discovery\n- **K8s mode**: Pod log monitoring in Kubernetes cluster\n  - Implements `container.Client` interface via Kubernetes API\n- **Agent mode**: Lightweight gRPC agent for remote log collection\n  - Run with `dozzle agent` or `bun run agent:dev`\n  - Listens on port 7007 with TLS certificate authentication\n\n## Key Architectural Patterns\n\n### Backend Abstraction Layers\n\nThe backend follows a clean layered architecture:\n\n```\nHTTP Handlers (internal/web)\n    ↓\nHostService Interface (internal/hostservice)\n    ↓\ncontainer.ClientService Interface (per host)\n    ↓\ncontainer.Client Interface\n    ↓\nImplementation (internal/container/{docker,k8s,agent})\n```\n\n**When adding new container operations:**\n\n1. Define method in `container.Client` interface (`internal/container/client.go`)\n2. Implement in `internal/container/docker/client.go` (and `internal/container/k8s/client.go` if applicable)\n3. Add it to `container.ClientService` (`internal/container/client_service.go`) and implement it in each\n   platform's `service.go` (`docker/`, `k8s/`, `agent/`)\n4. Add HTTP handler in `internal/web/` with appropriate route\n\n### Frontend Data Flow\n\n**Real-time Log Viewing:**\n\n1. User navigates to `/container/{id}` route\n2. Page component calls `useContainerStream(container)` composable\n3. Composable creates EventSource connection to `/api/containers/logs/stream?container={host}/{id}` (repeated for a merged view)\n4. Backend streams `LogEvent` objects via SSE\n5. Frontend buffers events (250ms debounce, max 1000ms)\n6. Batched buffer flushes update reactive `messages` array\n7. `LogViewer.vue` renders using appropriate component (`SimpleLogItem`, `ComplexLogItem`, `GroupedLogItem`)\n8. When messages exceed `maxLogs` (400), oldest entries replaced or marked as `SkippedLogsEntry`\n\n**Stats Streaming:**\n\n1. `container.ts` store connects to `/api/events/stream` on app init\n2. Backend multiplexes container events and stats into single SSE stream\n3. `container-stat` events update `Container._stat` and append to `_statsHistory`\n4. EMA calculation provides smoothed `movingAverageStat` (alpha=0.2)\n5. `ContainerTable.vue` displays mini bar charts using `statsHistory` with downsampling\n\n### Protocol Buffer Flow (Agent Mode)\n\n1. Main server creates `agent.NewClient(endpoint, certs)` for each remote host\n2. AgentClient implements `container.Client` interface\n3. Method calls translate to gRPC requests defined in `protos/rpc.proto`\n4. Remote agent receives gRPC call, delegates to local `docker.Client`\n5. Streaming RPCs (logs, stats, events) use bidirectional channels\n6. Responses converted back to domain models in `internal/container/agent/convert.go`\n\n### Cloud Tool Execution Flow\n\n1. `cloud.Client.Run()` blocks until `Notify()` signals a cloud dispatcher is configured\n2. `connect()` establishes bidirectional gRPC stream (`ToolStream` RPC) to cloud endpoint\n3. Cloud sends `ToolRequest` (ListTools or CallTool), client dispatches via `executeTool()`\n4. Tool calls run concurrently (max 5 via weighted semaphore), responses sent back on stream\n5. On disconnect, exponential backoff (1s→30s with jitter) triggers reconnection\n6. `PermissionDenied` errors stop retrying permanently (invalid API key / no pro plan)\n7. Tool definitions cached via `sync.Once`; zero overhead for non-cloud users\n\n### Log Parsing Pipeline\n\n1. Docker API returns multiplexed stream (8-byte headers + payload)\n2. `container/docker/log_reader.go` parses headers, extracts stdout/stderr type\n3. `container/logparse/event_generator.go` receives raw log lines\n4. Detection logic identifies:\n   - JSON structure → `ComplexLogEntry`\n   - Multi-line patterns (stack traces) → `GroupedLogEntry`\n   - Single lines → `SimpleLogEntry`\n5. Log level extraction via regex patterns\n6. `LogEvent` serialized to JSON and sent via SSE\n7. Frontend deserializes and renders with appropriate component\n\n## Adding New Features\n\n### Adding a New HTTP Route\n\n1. Define route in `internal/web/routes.go` using chi router:\n   ```go\n   r.Get(\"/api/custom-endpoint\", h.customHandler)\n   ```\n2. Implement handler method in appropriate file (e.g., `actions.go`, `logs.go`)\n3. Use `hostService` to find container/host via `FindContainer()` or `FindHost()`\n4. Return JSON response or establish SSE/WebSocket stream\n\n### Adding a New Log View Type\n\n1. Create route file in `assets/pages/` (e.g., `custom/[id].vue`)\n2. Create composable in `assets/composable/logs/eventStreams.ts` (e.g., `useCustomStream()`)\n3. Composable should:\n   - Build API URL with appropriate filters\n   - Create EventSource connection\n   - Handle buffering and message batching\n   - Return reactive `messages` array and control methods\n4. Use `LogViewer.vue` component to render messages\n5. Add backend API endpoint if needed (see above)\n\n### Adding Container Stats/Metrics\n\n1. Add field to `Stat` type in `internal/container/types.go`\n2. Update `container/docker/stats_collector.go` (and `container/k8s/stats_collector.go`) to extract the metric\n3. Add calculation logic in `container/docker/calculation.go` if needed\n4. Ensure protobuf definition includes field in `protos/rpc.proto`, and map it in `container/agent/convert.go`\n5. Frontend automatically receives updates via existing SSE stream\n6. Update `Container` model in `assets/models/Container.ts` if UI needs access\n\n### Working with Notifications/Alerts\n\n**Backend** (`internal/notification/`):\n\n- `manager.go`: Rule evaluation engine, manages alert state\n- `log_listener.go`: Subscribes to container log streams, evaluates rules against incoming logs\n- `types.go`: Alert rule definitions (log pattern matching, thresholds)\n- `dispatcher/`: Notification channel implementations\n\n**Frontend** (`assets/pages/notifications.vue`, `assets/components/notifications/`):\n\n- `AlertForm.vue`, `DestinationForm.vue`: UI for creating rules\n- Rules persisted to `./data/notifications.yml` via `internal/notification/persist.go`\n- Alert state displayed in notification cards\n\n**Adding a new notification channel:**\n\n1. Implement dispatcher interface in `internal/notification/dispatcher/`\n2. Register in `manager.go` dispatcher factory\n3. Add UI form in `assets/components/notifications/DestinationForm.vue`\n\n### Adding a New Cloud Tool\n\n1. Define the tool in `AvailableTools()` in `internal/cloud/tools.go` with name, description, and parameter schema\n2. Add a response message type in `protos/cloud.proto` and add it to the `CallToolResponse.result` oneof\n3. Run `make generate` to regenerate protobuf code\n4. Add a case in the `executeTool()` switch in `internal/cloud/tools.go`\n5. Implement the execution function in the appropriate `tools_*.go` file\n6. Use `ToolHostService` interface methods to access container/host data\n7. Add tests in `tools_test.go`\n\n## Common Development Patterns\n\n### Testing\n\n- Always run Go tests with race detector: `go test -race`\n- Frontend tests require `TZ=UTC` for timestamp consistency\n- Integration tests use Playwright with `make int` (runs docker-compose setup)\n- Use `testify/assert` for Go test assertions\n\n### Hot Reload Development\n\n- `make dev` runs both backend (air) and frontend (vite) with hot reload\n- `DEV=true` disables embedded asset serving\n- `LIVE_FS=true` serves assets from filesystem instead of embedded\n- Backend changes trigger air restart automatically\n- Frontend changes trigger vite HMR\n\n### Debugging\n\n- Backend logs: Set `--level debug` flag or `DOZZLE_LEVEL=debug` env var\n- Frontend: Vue DevTools browser extension\n- SSE streams: Browser DevTools Network tab shows EventSource connections\n","isInternal":false,"tokens":9341,"sizeBytes":38130},{"name":"bug-hunter.md","path":".claude/agents/bug-hunter.md","rawUrl":"https://raw.githubusercontent.com/amir20/dozzle/HEAD/.claude/agents/bug-hunter.md","title":"bug-hunter","category":"subagent-persona","format":"markdown","content":"---\nname: bug-hunter\ndescription: \"Use this agent when you need to review recently written or modified code for bugs, logic errors, edge cases, and unexpected behavior. This includes both Go backend code and Vue/TypeScript frontend code. This agent should be used after writing new features, fixing bugs, or refactoring code to catch issues before they reach production.\\\\n\\\\nExamples:\\\\n\\\\n- User writes a new HTTP handler in Go:\\\\n  user: \\\"I just added a new endpoint for container health checks\\\"\\\\n  assistant: \\\"Let me review the new code for potential bugs and edge cases.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to review the recently changed files>\\\\n\\\\n- User implements a new Vue composable:\\\\n  user: \\\"I created a new composable for managing WebSocket reconnection\\\"\\\\n  assistant: \\\"I'll launch the bug hunter to review your new composable for edge cases and potential issues.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to analyze the composable>\\\\n\\\\n- User modifies log parsing logic:\\\\n  user: \\\"I updated the event_generator.go to handle a new log format\\\"\\\\n  assistant: \\\"Let me have the bug hunter review your changes to ensure all edge cases in log parsing are covered.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to review the modified parsing code>\\\\n\\\\n- After a chunk of code is written during a feature implementation:\\\\n  assistant: \\\"I've finished implementing the notification dispatcher. Let me run the bug hunter to check for issues.\\\"\\\\n  <uses Task tool to launch bug-hunter agent proactively to review the new code>\"\nmodel: opus\ncolor: blue\nmemory: project\n---\n\nYou are an elite bug-hunting code reviewer with deep expertise in both Go backend systems and Vue 3/TypeScript frontend applications. You have extensive experience finding subtle bugs, race conditions, nil pointer dereferences, unhandled edge cases, type safety issues, and logic errors that slip past typical review. You think adversarially — always asking \"what could go wrong here?\"\n\n## Your Mission\n\nReview recently written or modified code to find bugs, logic errors, and uncovered edge cases. You focus on code that could fail at runtime, produce incorrect results, or behave unexpectedly under specific conditions.\n\n## Review Process\n\n1. **Identify Changed/New Code**: Use git status, git diff, or examine the files the user points you to. Focus on recently written code, not the entire codebase.\n\n2. **Analyze Each File Systematically**: For every file with changes, examine:\n   - Control flow paths (all branches of if/else, switch, select)\n   - Error handling (are errors checked? propagated correctly? logged?)\n   - Nil/undefined checks (pointer dereference in Go, optional chaining in TS)\n   - Concurrency safety (goroutine leaks, race conditions, channel operations)\n   - Resource cleanup (deferred closes, stream cleanup, event listener removal)\n   - Boundary conditions (empty arrays, zero values, max values, negative numbers)\n   - Type safety (type assertions in Go, type narrowing in TS)\n   - API contract adherence (correct HTTP status codes, proper SSE format, GraphQL schema alignment)\n\n3. **Cross-Layer Analysis**: Check that frontend and backend changes are consistent:\n   - API request/response shapes match between Go handlers and TypeScript types\n   - SSE event names and payload structures align\n   - GraphQL schema, resolvers, and frontend queries are in sync\n   - WebSocket message formats match on both sides\n\n## Go-Specific Bug Patterns to Check\n\n- **Nil pointer dereference**: Especially after type assertions, map lookups, and interface conversions\n- **Goroutine leaks**: Goroutines blocked on channels that are never closed or written to\n- **Race conditions**: Shared state accessed from multiple goroutines without synchronization\n- **Deferred closure in loops**: `defer` inside loops won't execute until function returns\n- **Error shadowing**: Using `:=` that shadows an outer `err` variable\n- **Slice/map initialization**: Operating on nil slices/maps (nil map write panics)\n- **Context cancellation**: Not respecting context.Done() in long-running operations\n- **HTTP response body leaks**: Not closing response bodies after HTTP calls\n- **Integer overflow**: Especially in stats calculations with uint64\n- **String/byte slice sharing**: Modifying a slice that shares underlying array\n- **Channel operations on nil channels**: Blocking forever on nil channel send/receive\n- **Mutex copy**: Passing sync.Mutex by value instead of pointer\n\n## Vue/TypeScript-Specific Bug Patterns to Check\n\n- **Reactive reference unwrapping**: Using `.value` correctly with `ref()` vs `reactive()`\n- **Computed dependency tracking**: Missing reactive dependencies in computed properties\n- **Watch cleanup**: Not cleaning up watchers, event listeners, or timers in `onUnmounted`\n- **SSE/EventSource cleanup**: Connections not properly closed on component unmount\n- **Array reactivity**: Using index assignment instead of reactive methods\n- **Optional chaining gaps**: Accessing nested properties without null checks\n- **Promise error handling**: Unhandled promise rejections, missing `.catch()` or try/catch\n- **Type narrowing issues**: Assuming a type without proper guards\n- **Stale closure references**: Callbacks capturing outdated reactive values\n- **Memory leaks**: Growing arrays without bounds (check maxLogs enforcement, statsHistory rolling window)\n- **Race conditions in async operations**: Component unmounted before async operation completes\n- **Incorrect v-if/v-show usage**: Rendering components that depend on data not yet loaded\n- **Event buffer overflow**: Not handling backpressure in SSE streams\n\n## Project-Specific Concerns\n\n- **EMA calculations**: Alpha=0.2 for stats smoothing — verify division by zero, NaN handling\n- **Rolling window (300 items)**: Ensure proper eviction and no off-by-one errors\n- **Log entry type discrimination**: `LogEntry.create()` must handle all `logEvent.t` values\n- **Multi-host operations**: Container/host lookups via `FindContainer()`/`FindHost()` may return nil\n- **Docker API version negotiation**: Calls may fail on older Docker versions\n- **Protobuf serialization**: `FromProto()` methods must handle nil/empty fields\n- **Authentication modes**: Code must work correctly in all three auth modes (none, simple, forward-proxy)\n- **SSE buffering**: 250ms debounce with 1000ms max — verify timer cleanup\n- **CPU normalization**: Division by `cpuLimit` or `nCPU` — check for zero values\n\n## Output Format\n\nFollow ultra-brief mode as specified in the project guidelines:\n\n- Critical issues only (bugs, security, blockers)\n- Brief bullet points, no lengthy explanations\n- Skip verbose sections (no \"Strengths\", \"Summary\", etc.)\n- Include file:line references when relevant\n- Maximum ~10-15 lines per response\n\nFor each bug found, report:\n\n- **File:line** — Brief description of the bug\n- **Severity**: 🔴 Critical (will crash/corrupt) | 🟡 Warning (could fail under specific conditions) | 🟠 Edge case (uncovered scenario)\n- One-line fix suggestion if obvious\n\nIf no bugs are found, say so concisely. Do not fabricate issues.\n\n## Approach\n\n1. First, determine what code was recently changed (check git diff or ask the user)\n2. Read the changed files carefully\n3. For each file, apply the relevant bug pattern checklist above\n4. Cross-reference frontend and backend changes for consistency\n5. Report findings in the ultra-brief format\n\nDo NOT review code style, naming conventions, or suggest refactors unless they mask a bug. Focus exclusively on correctness.\n\n**Update your agent memory** as you discover recurring bug patterns, common error-prone code paths, areas of the codebase with historical issues, and edge cases specific to this project's architecture. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.\n\nExamples of what to record:\n\n- Recurring nil-check omissions in specific packages\n- Components that frequently have cleanup issues\n- API endpoints with known edge case gaps\n- Stats calculation patterns that are error-prone\n- Log parsing paths that have caused issues before\n\n# Persistent Agent Memory\n\nYou have a persistent Persistent Agent Memory directory at `/Users/araminfar/Workspace/dozzle/.claude/agent-memory/bug-hunter/`. Its contents persist across conversations.\n\nAs you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.\n\nGuidelines:\n\n- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise\n- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md\n- Update or remove memories that turn out to be wrong or outdated\n- Organize memory semantically by topic, not chronologically\n- Use the Write and Edit tools to update your memory files\n\nWhat to save:\n\n- Stable patterns and conventions confirmed across multiple interactions\n- Key architectural decisions, important file paths, and project structure\n- User preferences for workflow, tools, and communication style\n- Solutions to recurring problems and debugging insights\n\nWhat NOT to save:\n\n- Session-specific context (current task details, in-progress work, temporary state)\n- Information that might be incomplete — verify against project docs before writing\n- Anything that duplicates or contradicts existing CLAUDE.md instructions\n- Speculative or unverified conclusions from reading a single file\n\nExplicit user requests:\n\n- When the user asks you to remember something across sessions (e.g., \"always use bun\", \"never auto-commit\"), save it — no need to wait for multiple interactions\n- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files\n- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project\n\n## MEMORY.md\n\nYour MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.\n","frontmatter":{"name":"bug-hunter","description":"Use this agent when you need to review recently written or modified code for bugs, logic errors, edge cases, and unexpected behavior. This includes both Go backend code and Vue/TypeScript frontend code. This agent should be used after writing new features, fixing bugs, or refactoring code to catch issues before they reach production.\\n\\nExamples:\\n\\n- User writes a new HTTP handler in Go:\\n  user: \"I just added a new endpoint for container health checks\"\\n  assistant: \"Let me review the new code for potential bugs and edge cases.\"\\n  <uses Task tool to launch bug-hunter agent to review the recently changed files>\\n\\n- User implements a new Vue composable:\\n  user: \"I created a new composable for managing WebSocket reconnection\"\\n  assistant: \"I'll launch the bug hunter to review your new composable for edge cases and potential issues.\"\\n  <uses Task tool to launch bug-hunter agent to analyze the composable>\\n\\n- User modifies log parsing logic:\\n  user: \"I updated the event_generator.go to handle a new log format\"\\n  assistant: \"Let me have the bug hunter review your changes to ensure all edge cases in log parsing are covered.\"\\n  <uses Task tool to launch bug-hunter agent to review the modified parsing code>\\n\\n- After a chunk of code is written during a feature implementation:\\n  assistant: \"I've finished implementing the notification dispatcher. Let me run the bug hunter to check for issues.\"\\n  <uses Task tool to launch bug-hunter agent proactively to review the new code>","model":"opus","color":"blue","memory":"project"},"isInternal":false,"tokens":2167,"sizeBytes":10284},{"name":"go-perf-reviewer.md","path":".claude/agents/go-perf-reviewer.md","rawUrl":"https://raw.githubusercontent.com/amir20/dozzle/HEAD/.claude/agents/go-perf-reviewer.md","title":"go-perf-reviewer","category":"subagent-persona","format":"markdown","content":"---\nname: go-perf-reviewer\ndescription: \"Use this agent when Go code has been written or modified and needs review for performance issues, inefficient patterns, or unnecessary resource loading. This agent focuses exclusively on Go backend code and should be triggered after changes to `.go` files.\\\\n\\\\nExamples:\\\\n\\\\n- user: \\\"I just added a new endpoint that fetches container stats\\\"\\\\n  assistant: \\\"Let me use the go-perf-reviewer agent to check the new endpoint for performance issues.\\\"\\\\n  <commentary>Since Go code was written for a new endpoint, use the Task tool to launch the go-perf-reviewer agent to review the changes for inefficient patterns.</commentary>\\\\n\\\\n- user: \\\"Can you review my changes to the Docker client wrapper?\\\"\\\\n  assistant: \\\"I'll launch the go-perf-reviewer agent to analyze your Docker client changes for performance concerns.\\\"\\\\n  <commentary>The user is asking for a review of Go code changes. Use the Task tool to launch the go-perf-reviewer agent to identify any performance anti-patterns.</commentary>\\\\n\\\\n- user: \\\"I refactored the log streaming pipeline\\\"\\\\n  assistant: \\\"Let me run the go-perf-reviewer agent against your refactored streaming code to catch any performance regressions.\\\"\\\\n  <commentary>Streaming code is performance-critical. Use the Task tool to launch the go-perf-reviewer agent to ensure the refactor doesn't introduce inefficiencies.</commentary>\"\nmodel: opus\ncolor: green\nmemory: project\n---\n\nYou are an expert Go performance engineer with deep knowledge of runtime internals, memory allocation patterns, garbage collector behavior, and idiomatic high-performance Go. You specialize in reviewing Go code for performance anti-patterns in long-running server applications that interact with Docker, gRPC, and streaming APIs.\n\n**Core Philosophy**: Dozzle must be lean and lazy by default. It should never load, allocate, compute, or fetch anything until it is actually needed. Every byte of memory and every CPU cycle matters in a lightweight monitoring tool.\n\n## Your Review Process\n\n1. **Examine only the changed Go files**. Use available tools to read the recent changes or diffs.\n2. **Identify specific performance issues** — do not comment on style, naming, or correctness unless it directly causes a performance problem.\n3. **Provide actionable feedback** with file:line references and concrete fix suggestions.\n\n## Patterns to Flag\n\n### Memory & Allocation\n\n- Unnecessary allocations in hot paths (loops, stream handlers, per-request code)\n- Slice/map pre-allocation missing when size is known or estimable\n- `append` in tight loops without pre-sized slices\n- String concatenation with `+` instead of `strings.Builder` in loops\n- Returning large structs by value instead of pointer when appropriate\n- Unnecessary copies of large data (e.g., ranging over slice of structs by value)\n- `[]byte` ↔ `string` conversions that could be avoided\n- Creating closures in loops that capture loop variables unnecessarily\n- Allocating buffers per-call that could be pooled via `sync.Pool`\n\n### Lazy Loading & Eager Initialization\n\n- **Top priority**: Loading data, making API calls, or initializing resources before they are actually needed\n- Fetching all containers/stats when only a subset is requested\n- Initializing clients, connections, or caches at startup that may never be used\n- Reading entire files/streams into memory when streaming/pagination would suffice\n- Computing derived data eagerly when it could be computed on demand\n- Pre-populating maps/caches with all possible entries instead of lazy-filling\n\n### Concurrency & Goroutines\n\n- Goroutine leaks: goroutines without proper cancellation via `context.Context`\n- Missing `defer cancel()` after `context.WithCancel/WithTimeout`\n- Unbounded goroutine spawning without semaphore/worker pool\n- Channel misuse: unbuffered channels causing unnecessary blocking, or oversized buffered channels wasting memory\n- Holding locks longer than necessary; lock contention in hot paths\n- Using `sync.Mutex` where `sync.RWMutex` would reduce contention\n\n### I/O & Streaming\n\n- Not using `bufio.Reader`/`bufio.Writer` for I/O operations\n- Reading entire HTTP response bodies into memory (`io.ReadAll`) when streaming is possible\n- Not closing response bodies, readers, or connections (resource leaks)\n- Blocking I/O without timeouts or context cancellation\n- Serializing/deserializing JSON repeatedly when it could be done once\n- Using `encoding/json` in ultra-hot paths where a faster serializer is warranted\n\n### Docker/gRPC Specific\n\n- Making redundant Docker API calls (e.g., inspecting a container multiple times)\n- Not using Docker API filters to narrow results server-side\n- Fetching all container logs when `tail` or `since` parameters should limit scope\n- gRPC streams not properly drained or closed\n- Creating new Docker/gRPC clients per request instead of reusing\n\n### General Go Anti-Patterns\n\n- `reflect` usage in hot paths\n- `fmt.Sprintf` for simple string operations where direct concatenation suffices\n- `interface{}` / `any` boxing causing heap escapes\n- Unnecessary use of `defer` in tight loops (small but real overhead)\n- `time.After` in select loops (creates new timer each iteration; use `time.NewTimer` + `Reset`)\n- Regex compilation inside functions instead of package-level `var` with `regexp.MustCompile`\n- Sorting large slices repeatedly instead of maintaining sorted order\n\n## Output Format\n\nUse ultra-brief mode as specified by the project:\n\n- Critical performance issues only\n- Brief bullet points with file:line references\n- Concrete suggestion for each issue\n- Maximum ~10-15 lines per response\n- No praise sections, no summaries, no fluff\n\nExample output:\n\n```\n- `internal/container/docker/client.go:142` — `io.ReadAll(resp.Body)` reads entire log stream into memory. Stream with `bufio.Scanner` instead.\n- `internal/web/logs.go:87` — New `json.Encoder` created per log line in hot loop. Reuse encoder or use `sync.Pool`.\n- `internal/hostservice/multi_host.go:53` — All hosts initialized eagerly at startup. Defer client creation until first access.\n```\n\nIf no performance issues are found, state that clearly in one line.\n\n**Update your agent memory** as you discover performance patterns, hot paths, allocation-heavy code paths, and architectural decisions in this codebase. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.\n\nExamples of what to record:\n\n- Hot paths identified (log streaming, stats collection, event processing)\n- Existing `sync.Pool` usage or buffer reuse patterns\n- Known allocation-heavy areas\n- Docker API call patterns and caching strategies\n- gRPC streaming patterns used in agent mode\n- Areas where lazy loading is already implemented vs. missing\n\n# Persistent Agent Memory\n\nYou have a persistent Persistent Agent Memory directory at `/Users/araminfar/Workspace/dozzle/.claude/agent-memory/go-perf-reviewer/`. Its contents persist across conversations.\n\nAs you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.\n\nGuidelines:\n\n- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise\n- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md\n- Update or remove memories that turn out to be wrong or outdated\n- Organize memory semantically by topic, not chronologically\n- Use the Write and Edit tools to update your memory files\n\nWhat to save:\n\n- Stable patterns and conventions confirmed across multiple interactions\n- Key architectural decisions, important file paths, and project structure\n- User preferences for workflow, tools, and communication style\n- Solutions to recurring problems and debugging insights\n\nWhat NOT to save:\n\n- Session-specific context (current task details, in-progress work, temporary state)\n- Information that might be incomplete — verify against project docs before writing\n- Anything that duplicates or contradicts existing CLAUDE.md instructions\n- Speculative or unverified conclusions from reading a single file\n\nExplicit user requests:\n\n- When the user asks you to remember something across sessions (e.g., \"always use bun\", \"never auto-commit\"), save it — no need to wait for multiple interactions\n- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files\n- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project\n\n## MEMORY.md\n\nYour MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.\n","frontmatter":{"name":"go-perf-reviewer","description":"Use this agent when Go code has been written or modified and needs review for performance issues, inefficient patterns, or unnecessary resource loading. This agent focuses exclusively on Go backend code and should be triggered after changes to `.go` files.\\n\\nExamples:\\n\\n- user: \"I just added a new endpoint that fetches container stats\"\\n  assistant: \"Let me use the go-perf-reviewer agent to check the new endpoint for performance issues.\"\\n  <commentary>Since Go code was written for a new endpoint, use the Task tool to launch the go-perf-reviewer agent to review the changes for inefficient patterns.</commentary>\\n\\n- user: \"Can you review my changes to the Docker client wrapper?\"\\n  assistant: \"I'll launch the go-perf-reviewer agent to analyze your Docker client changes for performance concerns.\"\\n  <commentary>The user is asking for a review of Go code changes. Use the Task tool to launch the go-perf-reviewer agent to identify any performance anti-patterns.</commentary>\\n\\n- user: \"I refactored the log streaming pipeline\"\\n  assistant: \"Let me run the go-perf-reviewer agent against your refactored streaming code to catch any performance regressions.\"\\n  <commentary>Streaming code is performance-critical. Use the Task tool to launch the go-perf-reviewer agent to ensure the refactor doesn't introduce inefficiencies.</commentary>","model":"opus","color":"green","memory":"project"},"isInternal":false,"tokens":1865,"sizeBytes":8927}],"systemPromptSnippet":"<yakaai_skills repo=\"amir20/dozzle\">\n<!-- File: .claude/skills/update-int-snapshots/SKILL.md (Tokens: ~777 | Category: anthropic-skill) -->\n---\nname: update-int-snapshots\ndescription: Use when Playwright visual snapshots in e2e/visual.spec.ts-snapshots/ need to be regenerated after a UI change (logo, theme, layout, anything that affects rendered pixels) and the integration suite needs to be rerun via make int\n---\n\n# Update Integration Snapshots\n\n## When to Use\n\n- A visual change landed (logo, color, spacing, fonts) and `make int` will fail on `visual.spec.ts` until snapshots are regenerated.\n- A snapshot test reports diffs you've confirmed are intentional.\n\nDo NOT use when diffs are unintentional regressions — investigate first.\n\n## Procedure\n\n1. **Delete the stale snapshots** so Playwright writes fresh ones (don't try to update in place, the old PNGs can confuse the diff):\n\n   ```bash\n   rm e2e/visual.spec.ts-snapshots/*.png\n   ```\n\n2. **Run with `--update-snapshots` via `compose up`.** You must use `compose up` (not `compose run`) because the navigation sidebar snapshot includes the live container list, and the two modes produce different sibling containers. The cleanest way is to patch the playwright command in `docker-compose.yml` temporarily:\n\n   ```bash\n   sed -i.bak 's|command: npx --yes playwright test|command: npx --yes playwright test --update-snapshots|' docker-compose.yml\n   make int\n   mv docker-compose.yml.bak docker-compose.yml\n   ```\n\n   Snapshots must be generated in Linux/Chromium (the compose image), not macOS, because filenames include the platform suffix (e.g. `-chromium-linux.png`).\n\n   Nothing needs clearing first. The compose file publishes no host ports, every container is named `dozzle_e2e_*` under the fixed project `dozzle_e2e`, and filters match those names exactly. Other containers on the machine can't block the run or show up in the snapshots, and a leftover run from another worktree is recreated. Two worktrees running `make int` at the same time do still clobber each other, so run one at a time.\n\n3. **Verify** by rerunning the normal suite:\n\n   ```bash\n   make int\n   ```\n\n   Should now pass cleanly.\n\n4. **Commit the regenerated PNGs** alongside the UI change so CI stays green.\n\n## Common Mistakes\n\n- **Running `npx playwright test --update-snapshots` locally on macOS.** Generates `-darwin` filenames CI doesn't use. Always go through the docker compose image.\n- **Forgetting to delete first.** `--update-snapshots` does overwrite, but if the test layout changed (new test, renamed snapshot), stale PNGs are left behind. Wipe-and-regenerate is safest.\n- **Adding a test container without the prefix, or a filter without anchors.** `name=dozzle` is a substring match and pulls in any local container with \"dozzle\" in its name. Use `container_name: dozzle_e2e_<service>` and `DOZZLE_FILTER=name=^dozzle_e2e_<service>$$` (`$$` escapes `$` in compose).\n- **Publishing a host port \"for debugging\".** It brings back conflicts with whatever else is on the machine. Reach instances by service name from inside the network.\n\n## Quick Reference\n\n```bash\nrm e2e/visual.spec.ts-snapshots/*.png\nsed -i.bak 's|command: npx --yes playwright test|& --update-snapshots|' docker-compose.yml\nmake int\nmv docker-compose.yml.bak docker-compose.yml\nmake int   # verify pass\n```\n\n<!-- File: CLAUDE.md (Tokens: ~9341 | Category: claude-rule) -->\n# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Response Style\n\nKeep chat responses brief: bullet points, `file:line` references, no \"Strengths\" or\n\"Summary\" sections. What the PR review bot looks for is set in\n`.github/workflows/claude-code-review.yml`, not here.\n\n## Translations\n\n`locales/en.yml` is the source of truth, but it is never the only file to change. Whenever you add, rename, or remove a key in `en.yml`, apply the same change to **every** other file in `locales/` in the same commit, with the string actually translated (not the English text copied over). Keys are also deleted everywhere when they lose their last consumer.\n\nThere is no Crowdin or Weblate sync in this repo, so a key that only exists in `en.yml` silently falls back to English for every other locale and nothing ever flags it.\n\n### Docs Translations\n\n`docs/` is translated into `zh`, `de`, `fr` and `es`. The English page under `docs/` is the source of truth, and `docs/<locale>/<same path>` mirrors it. Changing an English page means updating all four translations in the same commit.\n\nUnlike `locales/`, there is no fallback here. A stale translated page renders confidently wrong instructions rather than quietly showing English, so drift is worse than a missing key.\n\nEach translated file carries a `sourceHash` in its frontmatter recording the English source it was written from. `bun docs/scripts/check-translations.mjs` fails when they diverge, and runs in CI as the `Docs Translations` job. After actually translating the changed prose, re-stamp with:\n\n```bash\nbun docs/scripts/check-translations.mjs --update\n```\n\n`--update` only re-stamps hashes. It does not translate anything, so running it on an untranslated page turns CI green while leaving the page wrong. Translate first.\n\nInside translated pages, everything in a fenced code block stays byte-identical to English except natural-language comments, and internal links are locale-prefixed (`/guide/agent` becomes `/de/guide/agent`). The sidebar is not duplicated per locale: its structure lives in `docs/.vitepress/locales/structure.ts` and each locale file supplies only labels, so a new page needs one slug there plus one label per locale.\n\n## Testing Unreleased PRs\n\nWhen replying to a GitHub issue or discussion where the fix lives in an open PR, ask the reporter to test the pre-built image: `amir20/dozzle:pr-XXX` (XXX = PR number). CI builds a tagged image per PR, so reporters can verify without waiting for the next release.\n\n## GitHub Tone (issues, PRs, comments, discussions)\n\nWhen posting anything to GitHub, write like a human maintainer, not an AI assistant. Avoid telltale LLM patterns:\n\n- No em dashes or en dashes. Use commas, periods, or parentheses instead.\n- No \"Not X, but Y\" rhetorical contrasts.\n- No throat-clearing openers (\"Great point\", \"Makes sense\", \"Thanks for the detailed write-up\").\n- No closing summaries or recap sentences.\n- No bolded inline labels mid-paragraph (\"**Why:**\", \"**Note:**\").\n- Drop hedges (\"essentially\", \"basically\", \"essentially just\"). Say it plain.\n- Lowercase casual tone is fine. Contractions are fine. Short sentences are fine.\n- Don't over-explain tradeoffs. State the decision, give one reason, stop.\n\n## Scope Discipline\n\nSmall first versions win. Building is cheap here; reviewing and maintaining is not.\n\n- **Agree the scope as a changelog before any code.** For a new feature, write what\n  ships (user-visible bullets) and an explicit \"Not in this release\" list, and get it\n  approved. Anything not on the ship list is out of scope, however reasonable it looks.\n- **A plan is a menu, not a work order.** Build phase 1 only. Later phases start after\n  phase 1 has merged, unless the user asks for more.\n- **Reuse before you add.** A new table, RPC, proto field, flag, label or setting needs a\n  one-line reason why an existing one (labels, existing events, existing alert channels,\n  existing pages) can't do it. Prefer one delivery surface to several.\n- **Stop at ~1,000 lines.** If a PR's non-test, non-generated diff passes ~1,000 lines,\n  stop and check scope with the user before continuing.\n- **One feature, one PR per repo.** Unrelated work (e.g. a settings redesign during an\n  updates feature) gets its own PR off main, never a layer in the same stack. Avoid\n  stacks deeper than two; with a GitHub stack, merge from the bottom.\n- **Subagents and workflows get the out-of-scope list.** Every implementation prompt\n  includes \"not in scope\" and the rule: a deviation that adds behaviour is reported,\n  not built.\n- **Review fixes fix; they don't extend.** Addressing feedback must not add new\n  mechanisms. If a fix needs one, raise it as a scope question.\n- **Audit size before review, not after.** When a body of work is done, report its size\n  per feature (code, tests, generated) next to its user impact, so cuts happen before\n  review time is spent.\n\n## Project Overview\n\nDozzle is a lightweight, web-based Docker log viewer with real-time monitoring capabilities. It's a hybrid application with:\n\n- **Backend**: Go (HTTP server, Docker API client, WebSocket streaming)\n- **Frontend**: Vue 3 (SPA with Vite, TypeScript)\n\nThe application supports multiple deployment modes: standalone server, Docker Swarm, and Kubernetes (k8s).\n\n## Development Commands\n\n### Setup\n\n```bash\n# Install dependencies\nbun install\n\n# Generate certificates and protobuf files\nmake generate\n```\n\n### Development\n\n```bash\n# Run full development environment (backend + frontend with hot reload)\nmake dev\n\n# Same, on a free port trio derived from this checkout's path, so several worktrees\n# can each run an instance at once. Prints the URL it picked.\nmake dev-auto\n\n# Alternative: Run backend and frontend separately\nbun run watch:backend  # Go backend with air (port 3100)\nbun run watch:frontend # Vite dev server (port 3100)\n\n# Run in agent mode for development\nbun run agent:dev\n```\n\n### Building\n\n```bash\n# Build frontend assets\nbun run build\n# or\nmake dist\n\n# Build entire application (includes frontend build)\nmake build\n\n# Build Docker image\nmake docker\n```\n\n### Testing\n\n```bash\n# Run Go tests\nmake test\n\n# Run frontend tests (Vitest)\nbun run test\n# Run in watch mode\nTZ=UTC bun run test --watch\n\n# Type checking\nbun run typecheck\n\n# Lint (Tailwind classes only)\nbun run lint\nbun run lint --fix\n```\n\nAlways go through `bun run` for `test` and `build`. `bun test` and `bun build` are\nbun's own test runner and bundler, not the scripts in `package.json`, and they fail\nin confusing ways against this codebase.\n\n`bun run` does not mean the bun runtime: it follows each tool's `#!/usr/bin/env node`\nshebang unless the script says `bun --bun`. Every script in `package.json` that starts a\nJS tool carries that flag except `test` and `typecheck`, which need node: under `--bun` vitest's jsdom\nworkers fail to start and `vue-tsc` reports a false TS2614. `release` (bumpp) is\nuntested on bun and also left alone. Those are the only reasons node is still required.\n\n### Linting\n\nESLint here checks Tailwind classes and nothing else. It is not a general JS or TS\nlinter, and it should not become one: `tsconfig.json` already has `strict`,\n`strictNullChecks` and `noUnusedLocals`, so `vue-tsc` covers that ground, and prettier\nplus `prettier-plugin-tailwindcss` own formatting and class order. No stylistic rule\nbelongs in `eslint.config.js`, which is also why `eslint-config-prettier` is not needed.\n\nEvery rule the Tailwind plugin ships is listed in that config with the reason it is on\nor off, so add to that list rather than reaching for a preset.\n\n`eslint --fix` runs on staged `js`/`mjs`/`ts`/`mts`/`vue` in the pre-commit hook, before\nprettier so prettier gets the last word on formatting. Run `bun run lint --fix` by hand\nafter writing a batch of markup if you would rather not find out at commit time.\n\nTwo blind spots worth knowing, because a green run does not mean a clean tree:\n\n- `@apply` inside an SFC `<style>` block is never handed to the plugin, so a bad class\n  there is never reported.\n- The arbitrary-value rule only maps whole-number spacing steps. `gap-[2px]` and `w-[3px]`\n  go unreported even though `gap-0.5` and `w-0.75` exist.\n\n### Preview & Other\n\n```bash\n# Preview production build locally\nbun run preview\n# or\nmake preview\n\n# Run integration tests (Playwright)\nmake int\n```\n\n## Architecture\n\n### Backend (Go)\n\nThe Go backend is organized into these key packages:\n\n- **`internal/web/`** - HTTP server and routing layer\n  - Routes defined in `routes.go` using chi router\n  - WebSocket/SSE handlers for log streaming (`logs.go`)\n  - Authentication middleware and token management (`auth.go`)\n  - Container action handlers (`actions.go`)\n  - `sse/`: the SSE writer; `search/`: log filtering, highlighting and HTML escaping\n\n- **`internal/container/`** - The generic layer every platform implements\n  - `types.go`, `host.go`: domain models (`Container`, `Host`, `LogEvent`, `ContainerStat`)\n  - `client.go`: `container.Client`, the raw engine interface\n  - `client_service.go`: `container.ClientService`, the per-host contract the rest of the app uses\n  - `store.go`: the container cache docker and k8s share (its header comment explains who\n    may write to the map), split by concern into `store_events.go` (the event loop),\n    `store_refresh.go` (reconnect, list and reconcile) and `store_fanout.go`\n    (bounded delivery to subscribers)\n  - `collector_lifecycle.go`: the lazy start/idle-stop reference count both stats collectors use\n  - `logparse/`: the log pipeline both platforms feed (`event_generator.go` grouping and JSON\n    detection, `level_guesser.go`, `logfmt.go`, `timestamp_prefix.go`)\n  - One folder per platform, each holding that platform end to end:\n    - `docker/`: `client.go`, `log_reader.go`, `stats_collector.go`, and `service.go` (`docker.Service`)\n    - `k8s/`: `client.go`, `log_reader.go`, `stats_collector.go`, and `service.go` (`k8s.Service`)\n    - `agent/`: gRPC `client.go`/`server.go`, `convert.go` (proto conversion), `service.go`, and\n      generated `pb/` (protos in `protos/`)\n\n- **`internal/hostservice/`** - Orchestration across hosts, one per deployment mode\n  - `multi_host.go`: `MultiHostService` (server and swarm modes)\n  - `retriable_client_manager.go` (server), `swarm_client_manager.go` (swarm)\n  - `k8s_cluster.go`: `K8sClusterService` (k8s mode)\n\n- **`internal/cli/`** - Command-line parsing, subcommands, client wiring (`clients.go`)\n\n- **`internal/cloud/`** - Dozzle Cloud integration (tool execution engine)\n  - `client.go`: Bidirectional gRPC stream client with auto-reconnect and exponential backoff\n  - `tools.go`: Tool registration, dispatch (`executeTool`), and `ToolHostService` interface\n  - `tools_containers.go`: Container listing, finding, stats, and inspection tools\n  - `tools_logs.go`: Log fetching with level/query/regex filtering (max 100 lines)\n  - `tools_actions.go`: Container start/stop/restart actions (gated by `enableActions`)\n  - `tools_helpers.go`: Proto conversion utilities and host name resolution\n  - Uses `protos/cloud.proto` for service and message definitions\n\n- **`internal/auth/`** - Authentication providers\n  - Simple file-based auth (`simple.go`)\n  - Forward proxy auth (`proxy.go`)\n  - Role-based authorization (`roles.go`)\n\n- **`internal/notification/`** - Alert and notification system\n  - `manager.go`: Notification rule evaluation and dispatching\n  - `log_listener.go`: Log pattern matching for alerts\n  - `dispatcher/`: Notification channel implementations (email, webhook, etc.)\n\n- **`main.go`** - Application entry point with mode switching (server/swarm/k8s/agent)\n\n### Frontend (Vue 3)\n\nThe frontend uses file-based routing with these conventions:\n\n- **`assets/pages/`** - File-based routes (unplugin-vue-router)\n  - `container/[id].vue`: Single container view\n  - `merged/[ids].vue`: Multi-container merged view\n  - `host/[id].vue`: Host-level logs\n  - `service/[name].vue`: Swarm service logs\n  - `stack/[name].vue`: Docker stack logs\n  - `group/[name].vue`: Custom grouped logs\n\n- **`assets/components/`** - Vue components (auto-imported). See \"Where files go\" below.\n  - `ui/`: generic primitives (`BarChart.vue`, `MetricCard.vue`, `Popover.vue`, `UsageMeter.vue`, ...)\n  - `shell/`: app frame and the singletons `layouts/default.vue` mounts once\n  - `nav/`: everything inside the sidebar\n  - `search/`: the command palette and search surfaces\n  - `containers/`: `ContainerTable.vue`, the toolbar, the terminal\n  - `hosts/`: `HostCard.vue` and friends\n  - `logs/`: the stream, with `logs/entries/` holding one file per `LogEntry` type\n    (`SimpleLogItem.vue`, `ComplexLogItem.vue`, `GroupedLogItem.vue`,\n    `ContainerEventLogItem.vue`, `SkippedEntriesLogItem.vue`, `LoadMoreLogItem.vue`)\n  - `views/`: one file per route that renders a log view, mounted by `assets/pages/`\n  - `notifications/`: the rules the user wrote\n  - `cloud/`: memory, cross-instance and account, split into `rail/`, `chat/`, `history/`\n\n- **`assets/stores/`** - Pinia stores (auto-imported)\n  - `config.ts`: App configuration and feature flags (injected from backend HTML, frozen immutable)\n  - `container.ts`: Container state management with EventSource streaming (`/api/events/stream`)\n  - `hosts.ts`: Multi-host state\n  - `settings.ts`: User preferences (localStorage-backed via profileStorage)\n  - `pinned.ts`: Pinned container logs for side-by-side viewing\n  - `swarm.ts`, `k8s.ts`: Deployment mode-specific state\n  - `announcements.ts`: Feature announcements\n\n- **`assets/composable/`** - Vue composables (auto-imported), foldered to mirror the components\n  - `ui/`: `popover.ts`, `media.ts`, `timeTicker.ts` - no domain knowledge\n  - `app/`: shell and global session state (`drawer.ts`, `theme.ts`, `toast.ts`, ...)\n  - `logs/`: the stream pipeline (`eventStreams.ts` SSE with 250ms buffered flushing,\n    `historicalLogs.ts`, `logContext.ts` provide/inject, `scrollContext.ts`,\n    `visible.ts`, `duckdb.ts` for SQL over logs)\n  - `containers/`: `containerActions.ts`, `imageUpdate.ts`, ...\n  - `cloud/`, `notifications/`, `editor/`\n\n  The `AutoImport` `dirs` glob is `assets/composable/**`. It must stay recursive: a bare\n  directory is scanned one level deep, and nested composables silently stop being\n  auto-imported with no error. The `**` also globs `*.spec.ts`, which is harmless only\n  while no spec exports anything.\n\n- **`assets/modules/`** - Vue plugins\n  - `router.ts`: Vue Router configuration\n  - `pinia.ts`: Pinia store setup\n  - `i18n.ts`: Internationalization\n\n### Communication Flow\n\n1. **Real-time Logs**: Frontend establishes SSE connections to `/api/containers/logs/stream?container={host}/{id}` (repeated for a merged view)\n2. **Container Events**: SSE stream at `/api/events/stream` pushes container lifecycle events\n3. **Stats**: Real-time CPU/memory stats streamed via SSE alongside events\n4. **Actions**: POST to `/api/hosts/{host}/containers/{id}/actions/{action}` (start/stop/restart)\n5. **Terminal**: WebSocket connections for container attach/exec at `/api/hosts/{host}/containers/{id}/attach`\n\n### Build System\n\n- **Frontend**: Vite builds to `dist/` with manifest\n- **Backend**: Embeds `dist/` using Go embed directive\n- **Hot Reload**: In development, `DEV=true` disables embedded assets, `LIVE_FS=true` serves from filesystem\n- **Makefile**: Orchestrates builds and dependency generation\n\n## Dozzle and Dozzle Cloud\n\nCloud features are folded into the Dozzle UI rather than linked to. The split that\ndecides where a feature lands:\n\n> **Dozzle owns anything about the container in front of the user. Cloud owns\n> anything that is history, cross-instance, or account.**\n\nDozzle is optimized for the current view and can act on it; Cloud reviews everything\nat once. When a feature could live in either, ask which of those two questions it\nanswers.\n\n### Naming\n\nThe two Cloud surfaces must not blur into each other:\n\n- **Notifications** — the rules the user wrote, and what those rules did.\n- **Findings** — the rules the user never wrote, that Cloud noticed anyway.\n\nFindings are not alerts and are never listed alongside them.\n\n### What Cloud adds is memory, not features\n\nNothing Dozzle does locally today ever gets gated. Alerts already splice into the log\nstream (`AlertLogItem.vue`), they just die on refresh because nothing remembers them.\nCloud's contribution is that the same thing survives a reload, a restart, and a week —\nwhich is why an alert can come back later as a dot on a container row.\n\nSo a Cloud-less install shows an empty history section with one muted line saying what\nit would hold, not a locked card. A surface that has no local half at all (a findings\nlist) is mounted only when cloud is configured, rather than existing as a permanent\nupsell page in the nav.\n\n### Linking out\n\nLinking out to Cloud is fine, and expected, for anything Cloud genuinely does better:\nbilling and keys, the report archive, cross-instance rollups, deep-investigation\ntranscripts. `AlertHit.url` in `protos/cloud.proto` already carries the deep link.\n\nNever link out for something Dozzle could answer against the stream already on screen.\n\"Show me the lines\" — driving the historical scroll to the window a finding or alert\ndescribes — is the whole reason these surfaces live in Dozzle at all.\n\n### Gating\n\nPlan decisions are computed server-side in Cloud and handed to Dozzle already decided\n(`fixLocked`), never inferred from missing data — absence read as tier is how a paying\ncustomer gets shown an upsell for what they already bought. Dozzle renders the gate it\nis given and branches on nothing beyond `isPro` for cosmetics.\n\nOne upsell surface: `CloudPopover`. Everywhere else stays silent, per the design\nsystem's rule that `primary` is reserved for the single action a surface wants you to\ntake — a findings drawer wants \"Show me the lines\", not \"Upgrade\".\n\n## Design System\n\nThe UI is being converged on one visual language: clean, flat, and quiet. New or\nredesigned surfaces follow these rules; when touching an old surface, bring it along\nrather than matching what is already there.\n\n### Surfaces\n\n- A grouped surface is a **neutral panel**: `rounded-lg border border-base-content/15 bg-base-200/40`,\n  with rows separated by `divide-y divide-base-content/10`. Padding is `p-4` per row,\n  `p-2` for a row of link items. No shadows, no gradients, no `--depth`.\n- Floating surfaces (dropdowns, popovers, toasts) use `rounded-box border border-base-content/10 bg-base-200`\n  with `shadow-sm`/`shadow-lg`. `nav-menu-panel` in `main.css` is the sidebar's version.\n- Inside a panel, a hairline `<div class=\"bg-base-content/10 h-px\">` separates blocks when\n  `divide-y` does not apply (e.g. inside a dropdown's flow).\n- Interactive rows are `hover:bg-base-300 rounded-md px-2 py-1.5 transition-colors`, with a\n  leading icon at `size-4 opacity-60` and a trailing `mdi:open-in-new size-3.5 opacity-40`\n  when the row leaves the app.\n\n### Color and severity\n\n- **Severity rides on the icon, never on the surface.** A tinted circle\n  (`bg-error/10 text-error`, `bg-warning/10 text-warning`, `bg-info/10 text-info`) plus a\n  status dot carries the state; the panel behind it stays neutral so the text keeps full\n  contrast. Do not use daisyUI `alert alert-error` / `alert-warning` / `alert-success`:\n  a saturated block at drawer or page width shouts over everything near it.\n- Use `InlineNotice` (`assets/components/ui/InlineNotice.vue`) for an in-page notice and\n  `ToastModal` for a floating one. Both are neutral panels with a tinted glyph.\n- Status text is a `status-pill` (see `main.css`): a bordered, uppercase, mono chip in\n  `neutral`, `success`, `primary`, `secondary`, `warning` or `error`. Prefer it to `badge`.\n- `primary` is reserved for the single action a surface wants you to take. Two solid\n  primary buttons in one panel is a bug; the secondary one is a plain `btn`, and a\n  destructive secondary is `btn text-error`, not `btn-error`.\n\n### Typography and density\n\n- Body `text-sm`, secondary copy `text-base-content/60`, footnotes `text-xs` at\n  `text-base-content/40`. Drawer and page titles are `text-2xl font-bold` with a\n  `text-base-content/60` subtitle underneath.\n- Section headings inside a form are `FormStepHeading` (numbered chip + uppercase label);\n  standalone section headings are `text-base-content/60 font-semibold tracking-wide uppercase`.\n- Numbers, keys, IDs, periods, and percentages are `font-mono`. Emphasize the meaningful\n  half and mute the rest (`font-semibold` used, `text-base-content/40` for `/ limit`).\n- A read-only value is text, not a disabled input. Label left, mono value right.\n\n### Meters and charts\n\n- Usage meters are `UsageMeter` (`assets/components/ui/UsageMeter.vue`): a\n  `bg-base-content/10 h-1.5 rounded-full` track with a `bg-primary` fill that turns\n  `bg-warning` past 70% and `bg-error` past 90%. Do not use daisyUI `<progress>` for these;\n  it is taller than the type around it and carries its own palette.\n- Charts stay custom and lightweight (`BarChart.vue`); no chart library.\n\n### Affordances\n\n- Tailwind v4's preflight sets `cursor: default` on buttons, so a control reads as\n  painted on unless something puts the pointer back. One base-layer rule in `main.css`\n  does that for `button`, `summary`, `select`, `[role=\"button\"]`, `.btn` and\n  checkbox/radio labels, skipping disabled ones. Never add `cursor-pointer` to a new\n  button; if a clickable thing still shows an arrow, it is a `div` pretending to be a\n  button and the fix is to make it a `<button>`.\n- Anything clickable needs a hover state and a hit area a pointer can find. Under ~24px\n  the painted box is not the target: draw the hit area with an `::after` inset so\n  the layout around it does not move (`AlertDot.vue`).\n\n### Motion\n\n- Transitions are `transition-colors` on hover, `duration-500` on a meter's width, and\n  `200ms cubic-bezier(0.34, 1.56, 0.64, 1)` for the icon spring in `main.css`.\n- Icon-only buttons opt into the spring with `btn-circle`, `btn-square` or `icon-btn`; the\n  flavours (`icon-float`, `icon-spin`, `icon-wiggle`, `icon-ring`) are set on the glyph.\n- Every animation has a `prefers-reduced-motion` escape.\n\n### Mobile\n\n- Under `isMobile` the shell is a floating tab bar (`shell/MobileTabBar.vue`: Home, Browse,\n  Notifications, Settings, plus a separate search button). A log route is a pushed screen:\n  no tab bar, and `ScrollableView`'s header carries a back button. Which routes count as log\n  routes is `LOG_ROUTES` in `composable/app/mobileShell.ts`; a new log view goes there.\n- Floating chrome on a phone uses the `frosted` utility (translucent fill + backdrop blur),\n  which falls back to a solid `base-200` without `backdrop-filter` or under\n  `prefers-reduced-transparency`. It is a fill only; add the border and shadow yourself. Do\n  not use daisyUI's `glass`, which paints a diagonal sheen. Panels and rows stay flat.\n- A menu that is an action list opens as a bottom sheet on a phone: pass `sheet` to `Popover`.\n\n### Consistency rules\n\n- A panel keeps its shape across states. Loading, error and healthy branches of the same\n  surface share the header, dividers and footer so nothing reshuffles when state changes.\n- The same concept looks the same everywhere. The cloud account panel is the same parts at\n  three widths: `CloudPopover.vue` (compact), `CloudSettingsCard.vue`, and\n  `notifications/CloudDestinationForm.vue`. When a fourth surface needs it, extract a\n  component instead of copying the classes.\n- Do not repeat the container's title inside its own content: a drawer header already names\n  the thing, so the panel below leads with what the header cannot say.\n- Drawer footers are sticky, opaque and full-bleed, bled over the drawer's `p-6` and padded\n  clear of the home indicator:\n  `bg-base-100 border-base-content/10 sticky -bottom-6 z-10 -mx-6 -mb-6 mt-auto border-t px-10 pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]`\n  (widen `-mx`/`-mb` by any padding the occupant adds of its own).\n\n## Where files go\n\nFour tests for a new component, applied in order. The first that matches decides the\nfolder, so a contributor never has to ask.\n\n1. **`ui/`** — it imports nothing under `@/` except `@/composable/ui/`, **and** its name\n   carries no Dozzle noun (container, host, log, alert, cloud, nav, swarm, k8s, stack,\n   service, chat, rail). `ui/` means \"would still build pasted into a different Vue app\".\n   It does not mean \"shared\" and it does not mean \"small\": a `ui/` component with one\n   consumer is fine, and a large domain component with six consumers still lives in its\n   feature folder. Genericity is the test, popularity is not.\n2. **`cloud/`** — the surface does not exist at all in a cloud-less install. Two carve-outs,\n   settled once: anything `assets/models/LogEntry.ts` registers is a log entry and stays in\n   `logs/entries/` with its siblings, `AlertLogItem.vue` included, because alerts splice\n   into the local stream. And a variant that plugs into a local form stays with that form,\n   which is why `CloudDestinationForm.vue` sits beside `WebhookDestinationForm.vue`.\n3. **`views/`** — a file in `assets/pages/` mounts it as that route's whole body. One file\n   per route. (`ContainerLog.vue` is also mounted by `layouts/default.vue` for pinned\n   columns; it is still that route's view.)\n4. Otherwise the **feature folder** named by the noun in the component's own name.\n\n`nav/` is everything inside the sidebar; `shell/` is the frame that positions it. Nest one\nlevel inside a feature folder only past ~15 files and only with an obvious sub-noun, never\ntwo. A spec moves with its subject. `assets/composable/` uses the same folder names and the\nsame first two tests.\n\n`components/ui/` may depend on `composable/ui/` and third-party code, and nothing else.\nThat is the one dependency edge worth enforcing in review.\n\nMoving a component is cheap: `unplugin-vue-components` runs without `directoryAsNamespace`,\nso a component's name is its filename and nesting is invisible to every template. It also\nmeans **two components may never share a basename**, at any depth.\n\n## Important Development Notes\n\n### Frontend\n\n- Auto-imports are configured for Vue composables, components, and Pinia stores (see `vite.config.ts`)\n- Icons use unplugin-icons with multiple icon sets (mdi, carbon, material-symbols, etc.)\n- Tailwind CSS with DaisyUI for styling\n- TypeScript definitions auto-generated in `assets/auto-imports.d.ts` and `assets/components.d.ts`\n- **Log Entry Types**: Three types of log messages supported\n  - `SimpleLogEntry`: Single-line text logs (`string`)\n  - `ComplexLogEntry`: Structured JSON logs (`JSONObject`)\n  - `GroupedLogEntry`: Multi-line grouped logs (`string[]`)\n- **Type consistency**: Use `LogMessage` type alias instead of `string | string[] | JSONObject` for log entry messages\n- **Log Entry Factory Pattern**: Use `LogEntry.create(logEvent)` to instantiate the correct entry type based on `logEvent.t` field\n- **EventSource Buffering**: Log streams use buffer-based flushing (250ms debounce, 1000ms max) to batch UI updates\n- **Charts/Visualizations**: Custom lightweight implementations (no D3.js)\n  - `BarChart.vue`: Self-contained bar chart with responsive downsampling\n  - Downsampling algorithm: Averages data into buckets based on available screen width\n  - All stat history tracked in `Container.statsHistory` (max 300 items via rolling window)\n  - `chartData` is always a rolling window of max 300 items — array length stays constant\n  - Uses `ref` (not `computed`) for `downsampledBars` to enable in-place mutation of the last bar, avoiding full re-renders\n  - Component instance is reused when switching containers; after init the chart only patches the last bar per tick, so on a wholesale `chartData` replacement (container switch) the parent must call the exposed `recalculate()`. `MultiContainerStat` holds refs to its `BarChart`s and calls it in the `containers` watch. (Note: `Container` carries Vue `ref`s, so VueTestUtils `setProps` cannot retrigger such a watch — tests must swap the container via a parent `ref` re-render.)\n\n### Backend\n\n- The application uses Go 1.25+ with module support\n- Certificate generation is required (`make generate` creates shared_key.pem and shared_cert.pem)\n- Protocol buffer generation happens via `go generate` directive in `main.go`\n- Docker client uses API version negotiation for compatibility\n- **Service Layer Architecture**:\n  - `container.ClientService` interface abstracts Docker/K8s/Agent backends\n  - `hostservice.MultiHostService` orchestrates multi-host operations\n  - `ClientManager` implementations: `RetriableClientManager` (server mode), `SwarmClientManager` (swarm mode)\n- **Where backend code goes**: code that only one platform needs lives in `internal/container/<platform>/`;\n  code docker and k8s share lives in `internal/container/` (or `logparse/` for the log pipeline);\n  code that coordinates several hosts lives in `internal/hostservice/`. `internal/container` never\n  imports a platform package, and platforms never import each other.\n\n### Authentication\n\n- Three modes: none, simple (file-based users.yml), forward-proxy (e.g., Authelia)\n- JWT tokens for simple auth with configurable TTL\n- User file location: `./data/users.yml` or `./data/users.yaml`\n\n### Testing\n\n- Go tests use standard `testing` package with testify assertions\n- Frontend uses Vitest with `@vue/test-utils`\n- Integration tests with Playwright in `e2e/`\n- Tests must run with `TZ=UTC` for consistent timestamps\n\n### Container Stats & Metrics\n\n- Stats are tracked using exponential moving average (EMA) with alpha=0.2\n- History stored in rolling window (300 items max) via `useSimpleRefHistory`\n- CPU metrics normalized by core count (respects `cpuLimit` or falls back to host `nCPU`)\n- Memory metrics include both percentage and absolute usage (`memoryUsage` vs `memory`)\n- Stats visualization uses adaptive downsampling for performance\n\n### Container Labels\n\n- `dev.dozzle.name`: Custom container display name\n- `dev.dozzle.group`: Group containers together\n- `dev.dozzle.url`: Link a container to its own web UI (http/https only; `Container.url` in `assets/models/Container.ts`)\n- Label-based filtering throughout the application\n\n### Deployment Modes\n\n- **Server mode** (default): Single or multi-host Docker monitoring\n  - Uses `RetriableClientManager` with local + remote agent clients\n- **Swarm mode**: Automatic discovery of Swarm nodes via Docker API\n  - Creates gRPC agent server on each node (port 7007)\n  - Uses `SwarmClientManager` for node discovery\n- **K8s mode**: Pod log monitoring in Kubernetes cluster\n  - Implements `container.Client` interface via Kubernetes API\n- **Agent mode**: Lightweight gRPC agent for remote log collection\n  - Run with `dozzle agent` or `bun run agent:dev`\n  - Listens on port 7007 with TLS certificate authentication\n\n## Key Architectural Patterns\n\n### Backend Abstraction Layers\n\nThe backend follows a clean layered architecture:\n\n```\nHTTP Handlers (internal/web)\n    ↓\nHostService Interface (internal/hostservice)\n    ↓\ncontainer.ClientService Interface (per host)\n    ↓\ncontainer.Client Interface\n    ↓\nImplementation (internal/container/{docker,k8s,agent})\n```\n\n**When adding new container operations:**\n\n1. Define method in `container.Client` interface (`internal/container/client.go`)\n2. Implement in `internal/container/docker/client.go` (and `internal/container/k8s/client.go` if applicable)\n3. Add it to `container.ClientService` (`internal/container/client_service.go`) and implement it in each\n   platform's `service.go` (`docker/`, `k8s/`, `agent/`)\n4. Add HTTP handler in `internal/web/` with appropriate route\n\n### Frontend Data Flow\n\n**Real-time Log Viewing:**\n\n1. User navigates to `/container/{id}` route\n2. Page component calls `useContainerStream(container)` composable\n3. Composable creates EventSource connection to `/api/containers/logs/stream?container={host}/{id}` (repeated for a merged view)\n4. Backend streams `LogEvent` objects via SSE\n5. Frontend buffers events (250ms debounce, max 1000ms)\n6. Batched buffer flushes update reactive `messages` array\n7. `LogViewer.vue` renders using appropriate component (`SimpleLogItem`, `ComplexLogItem`, `GroupedLogItem`)\n8. When messages exceed `maxLogs` (400), oldest entries replaced or marked as `SkippedLogsEntry`\n\n**Stats Streaming:**\n\n1. `container.ts` store connects to `/api/events/stream` on app init\n2. Backend multiplexes container events and stats into single SSE stream\n3. `container-stat` events update `Container._stat` and append to `_statsHistory`\n4. EMA calculation provides smoothed `movingAverageStat` (alpha=0.2)\n5. `ContainerTable.vue` displays mini bar charts using `statsHistory` with downsampling\n\n### Protocol Buffer Flow (Agent Mode)\n\n1. Main server creates `agent.NewClient(endpoint, certs)` for each remote host\n2. AgentClient implements `container.Client` interface\n3. Method calls translate to gRPC requests defined in `protos/rpc.proto`\n4. Remote agent receives gRPC call, delegates to local `docker.Client`\n5. Streaming RPCs (logs, stats, events) use bidirectional channels\n6. Responses converted back to domain models in `internal/container/agent/convert.go`\n\n### Cloud Tool Execution Flow\n\n1. `cloud.Client.Run()` blocks until `Notify()` signals a cloud dispatcher is configured\n2. `connect()` establishes bidirectional gRPC stream (`ToolStream` RPC) to cloud endpoint\n3. Cloud sends `ToolRequest` (ListTools or CallTool), client dispatches via `executeTool()`\n4. Tool calls run concurrently (max 5 via weighted semaphore), responses sent back on stream\n5. On disconnect, exponential backoff (1s→30s with jitter) triggers reconnection\n6. `PermissionDenied` errors stop retrying permanently (invalid API key / no pro plan)\n7. Tool definitions cached via `sync.Once`; zero overhead for non-cloud users\n\n### Log Parsing Pipeline\n\n1. Docker API returns multiplexed stream (8-byte headers + payload)\n2. `container/docker/log_reader.go` parses headers, extracts stdout/stderr type\n3. `container/logparse/event_generator.go` receives raw log lines\n4. Detection logic identifies:\n   - JSON structure → `ComplexLogEntry`\n   - Multi-line patterns (stack traces) → `GroupedLogEntry`\n   - Single lines → `SimpleLogEntry`\n5. Log level extraction via regex patterns\n6. `LogEvent` serialized to JSON and sent via SSE\n7. Frontend deserializes and renders with appropriate component\n\n## Adding New Features\n\n### Adding a New HTTP Route\n\n1. Define route in `internal/web/routes.go` using chi router:\n   ```go\n   r.Get(\"/api/custom-endpoint\", h.customHandler)\n   ```\n2. Implement handler method in appropriate file (e.g., `actions.go`, `logs.go`)\n3. Use `hostService` to find container/host via `FindContainer()` or `FindHost()`\n4. Return JSON response or establish SSE/WebSocket stream\n\n### Adding a New Log View Type\n\n1. Create route file in `assets/pages/` (e.g., `custom/[id].vue`)\n2. Create composable in `assets/composable/logs/eventStreams.ts` (e.g., `useCustomStream()`)\n3. Composable should:\n   - Build API URL with appropriate filters\n   - Create EventSource connection\n   - Handle buffering and message batching\n   - Return reactive `messages` array and control methods\n4. Use `LogViewer.vue` component to render messages\n5. Add backend API endpoint if needed (see above)\n\n### Adding Container Stats/Metrics\n\n1. Add field to `Stat` type in `internal/container/types.go`\n2. Update `container/docker/stats_collector.go` (and `container/k8s/stats_collector.go`) to extract the metric\n3. Add calculation logic in `container/docker/calculation.go` if needed\n4. Ensure protobuf definition includes field in `protos/rpc.proto`, and map it in `container/agent/convert.go`\n5. Frontend automatically receives updates via existing SSE stream\n6. Update `Container` model in `assets/models/Container.ts` if UI needs access\n\n### Working with Notifications/Alerts\n\n**Backend** (`internal/notification/`):\n\n- `manager.go`: Rule evaluation engine, manages alert state\n- `log_listener.go`: Subscribes to container log streams, evaluates rules against incoming logs\n- `types.go`: Alert rule definitions (log pattern matching, thresholds)\n- `dispatcher/`: Notification channel implementations\n\n**Frontend** (`assets/pages/notifications.vue`, `assets/components/notifications/`):\n\n- `AlertForm.vue`, `DestinationForm.vue`: UI for creating rules\n- Rules persisted to `./data/notifications.yml` via `internal/notification/persist.go`\n- Alert state displayed in notification cards\n\n**Adding a new notification channel:**\n\n1. Implement dispatcher interface in `internal/notification/dispatcher/`\n2. Register in `manager.go` dispatcher factory\n3. Add UI form in `assets/components/notifications/DestinationForm.vue`\n\n### Adding a New Cloud Tool\n\n1. Define the tool in `AvailableTools()` in `internal/cloud/tools.go` with name, description, and parameter schema\n2. Add a response message type in `protos/cloud.proto` and add it to the `CallToolResponse.result` oneof\n3. Run `make generate` to regenerate protobuf code\n4. Add a case in the `executeTool()` switch in `internal/cloud/tools.go`\n5. Implement the execution function in the appropriate `tools_*.go` file\n6. Use `ToolHostService` interface methods to access container/host data\n7. Add tests in `tools_test.go`\n\n## Common Development Patterns\n\n### Testing\n\n- Always run Go tests with race detector: `go test -race`\n- Frontend tests require `TZ=UTC` for timestamp consistency\n- Integration tests use Playwright with `make int` (runs docker-compose setup)\n- Use `testify/assert` for Go test assertions\n\n### Hot Reload Development\n\n- `make dev` runs both backend (air) and frontend (vite) with hot reload\n- `DEV=true` disables embedded asset serving\n- `LIVE_FS=true` serves assets from filesystem instead of embedded\n- Backend changes trigger air restart automatically\n- Frontend changes trigger vite HMR\n\n### Debugging\n\n- Backend logs: Set `--level debug` flag or `DOZZLE_LEVEL=debug` env var\n- Frontend: Vue DevTools browser extension\n- SSE streams: Browser DevTools Network tab shows EventSource connections\n\n<!-- File: .claude/agents/bug-hunter.md (Tokens: ~2167 | Category: subagent-persona) -->\n---\nname: bug-hunter\ndescription: \"Use this agent when you need to review recently written or modified code for bugs, logic errors, edge cases, and unexpected behavior. This includes both Go backend code and Vue/TypeScript frontend code. This agent should be used after writing new features, fixing bugs, or refactoring code to catch issues before they reach production.\\\\n\\\\nExamples:\\\\n\\\\n- User writes a new HTTP handler in Go:\\\\n  user: \\\"I just added a new endpoint for container health checks\\\"\\\\n  assistant: \\\"Let me review the new code for potential bugs and edge cases.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to review the recently changed files>\\\\n\\\\n- User implements a new Vue composable:\\\\n  user: \\\"I created a new composable for managing WebSocket reconnection\\\"\\\\n  assistant: \\\"I'll launch the bug hunter to review your new composable for edge cases and potential issues.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to analyze the composable>\\\\n\\\\n- User modifies log parsing logic:\\\\n  user: \\\"I updated the event_generator.go to handle a new log format\\\"\\\\n  assistant: \\\"Let me have the bug hunter review your changes to ensure all edge cases in log parsing are covered.\\\"\\\\n  <uses Task tool to launch bug-hunter agent to review the modified parsing code>\\\\n\\\\n- After a chunk of code is written during a feature implementation:\\\\n  assistant: \\\"I've finished implementing the notification dispatcher. Let me run the bug hunter to check for issues.\\\"\\\\n  <uses Task tool to launch bug-hunter agent proactively to review the new code>\"\nmodel: opus\ncolor: blue\nmemory: project\n---\n\nYou are an elite bug-hunting code reviewer with deep expertise in both Go backend systems and Vue 3/TypeScript frontend applications. You have extensive experience finding subtle bugs, race conditions, nil pointer dereferences, unhandled edge cases, type safety issues, and logic errors that slip past typical review. You think adversarially — always asking \"what could go wrong here?\"\n\n## Your Mission\n\nReview recently written or modified code to find bugs, logic errors, and uncovered edge cases. You focus on code that could fail at runtime, produce incorrect results, or behave unexpectedly under specific conditions.\n\n## Review Process\n\n1. **Identify Changed/New Code**: Use git status, git diff, or examine the files the user points you to. Focus on recently written code, not the entire codebase.\n\n2. **Analyze Each File Systematically**: For every file with changes, examine:\n   - Control flow paths (all branches of if/else, switch, select)\n   - Error handling (are errors checked? propagated correctly? logged?)\n   - Nil/undefined checks (pointer dereference in Go, optional chaining in TS)\n   - Concurrency safety (goroutine leaks, race conditions, channel operations)\n   - Resource cleanup (deferred closes, stream cleanup, event listener removal)\n   - Boundary conditions (empty arrays, zero values, max values, negative numbers)\n   - Type safety (type assertions in Go, type narrowing in TS)\n   - API contract adherence (correct HTTP status codes, proper SSE format, GraphQL schema alignment)\n\n3. **Cross-Layer Analysis**: Check that frontend and backend changes are consistent:\n   - API request/response shapes match between Go handlers and TypeScript types\n   - SSE event names and payload structures align\n   - GraphQL schema, resolvers, and frontend queries are in sync\n   - WebSocket message formats match on both sides\n\n## Go-Specific Bug Patterns to Check\n\n- **Nil pointer dereference**: Especially after type assertions, map lookups, and interface conversions\n- **Goroutine leaks**: Goroutines blocked on channels that are never closed or written to\n- **Race conditions**: Shared state accessed from multiple goroutines without synchronization\n- **Deferred closure in loops**: `defer` inside loops won't execute until function returns\n- **Error shadowing**: Using `:=` that shadows an outer `err` variable\n- **Slice/map initialization**: Operating on nil slices/maps (nil map write panics)\n- **Context cancellation**: Not respecting context.Done() in long-running operations\n- **HTTP response body leaks**: Not closing response bodies after HTTP calls\n- **Integer overflow**: Especially in stats calculations with uint64\n- **String/byte slice sharing**: Modifying a slice that shares underlying array\n- **Channel operations on nil channels**: Blocking forever on nil channel send/receive\n- **Mutex copy**: Passing sync.Mutex by value instead of pointer\n\n## Vue/TypeScript-Specific Bug Patterns to Check\n\n- **Reactive reference unwrapping**: Using `.value` correctly with `ref()` vs `reactive()`\n- **Computed dependency tracking**: Missing reactive dependencies in computed properties\n- **Watch cleanup**: Not cleaning up watchers, event listeners, or timers in `onUnmounted`\n- **SSE/EventSource cleanup**: Connections not properly closed on component unmount\n- **Array reactivity**: Using index assignment instead of reactive methods\n- **Optional chaining gaps**: Accessing nested properties without null checks\n- **Promise error handling**: Unhandled promise rejections, missing `.catch()` or try/catch\n- **Type narrowing issues**: Assuming a type without proper guards\n- **Stale closure references**: Callbacks capturing outdated reactive values\n- **Memory leaks**: Growing arrays without bounds (check maxLogs enforcement, statsHistory rolling window)\n- **Race conditions in async operations**: Component unmounted before async operation completes\n- **Incorrect v-if/v-show usage**: Rendering components that depend on data not yet loaded\n- **Event buffer overflow**: Not handling backpressure in SSE streams\n\n## Project-Specific Concerns\n\n- **EMA calculations**: Alpha=0.2 for stats smoothing — verify division by zero, NaN handling\n- **Rolling window (300 items)**: Ensure proper eviction and no off-by-one errors\n- **Log entry type discrimination**: `LogEntry.create()` must handle all `logEvent.t` values\n- **Multi-host operations**: Container/host lookups via `FindContainer()`/`FindHost()` may return nil\n- **Docker API version negotiation**: Calls may fail on older Docker versions\n- **Protobuf serialization**: `FromProto()` methods must handle nil/empty fields\n- **Authentication modes**: Code must work correctly in all three auth modes (none, simple, forward-proxy)\n- **SSE buffering**: 250ms debounce with 1000ms max — verify timer cleanup\n- **CPU normalization**: Division by `cpuLimit` or `nCPU` — check for zero values\n\n## Output Format\n\nFollow ultra-brief mode as specified in the project guidelines:\n\n- Critical issues only (bugs, security, blockers)\n- Brief bullet points, no lengthy explanations\n- Skip verbose sections (no \"Strengths\", \"Summary\", etc.)\n- Include file:line references when relevant\n- Maximum ~10-15 lines per response\n\nFor each bug found, report:\n\n- **File:line** — Brief description of the bug\n- **Severity**: 🔴 Critical (will crash/corrupt) | 🟡 Warning (could fail under specific conditions) | 🟠 Edge case (uncovered scenario)\n- One-line fix suggestion if obvious\n\nIf no bugs are found, say so concisely. Do not fabricate issues.\n\n## Approach\n\n1. First, determine what code was recently changed (check git diff or ask the user)\n2. Read the changed files carefully\n3. For each file, apply the relevant bug pattern checklist above\n4. Cross-reference frontend and backend changes for consistency\n5. Report findings in the ultra-brief format\n\nDo NOT review code style, naming conventions, or suggest refactors unless they mask a bug. Focus exclusively on correctness.\n\n**Update your agent memory** as you discover recurring bug patterns, common error-prone code paths, areas of the codebase with historical issues, and edge cases specific to this project's architecture. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.\n\nExamples of what to record:\n\n- Recurring nil-check omissions in specific packages\n- Components that frequently have cleanup issues\n- API endpoints with known edge case gaps\n- Stats calculation patterns that are error-prone\n- Log parsing paths that have caused issues before\n\n# Persistent Agent Memory\n\nYou have a persistent Persistent Agent Memory directory at `/Users/araminfar/Workspace/dozzle/.claude/agent-memory/bug-hunter/`. Its contents persist across conversations.\n\nAs you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.\n\nGuidelines:\n\n- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise\n- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md\n- Update or remove memories that turn out to be wrong or outdated\n- Organize memory semantically by topic, not chronologically\n- Use the Write and Edit tools to update your memory files\n\nWhat to save:\n\n- Stable patterns and conventions confirmed across multiple interactions\n- Key architectural decisions, important file paths, and project structure\n- User preferences for workflow, tools, and communication style\n- Solutions to recurring problems and debugging insights\n\nWhat NOT to save:\n\n- Session-specific context (current task details, in-progress work, temporary state)\n- Information that might be incomplete — verify against project docs before writing\n- Anything that duplicates or contradicts existing CLAUDE.md instructions\n- Speculative or unverified conclusions from reading a single file\n\nExplicit user requests:\n\n- When the user asks you to remember something across sessions (e.g., \"always use bun\", \"never auto-commit\"), save it — no need to wait for multiple interactions\n- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files\n- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project\n\n## MEMORY.md\n\nYour MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.\n\n<!-- File: .claude/agents/go-perf-reviewer.md (Tokens: ~1865 | Category: subagent-persona) -->\n---\nname: go-perf-reviewer\ndescription: \"Use this agent when Go code has been written or modified and needs review for performance issues, inefficient patterns, or unnecessary resource loading. This agent focuses exclusively on Go backend code and should be triggered after changes to `.go` files.\\\\n\\\\nExamples:\\\\n\\\\n- user: \\\"I just added a new endpoint that fetches container stats\\\"\\\\n  assistant: \\\"Let me use the go-perf-reviewer agent to check the new endpoint for performance issues.\\\"\\\\n  <commentary>Since Go code was written for a new endpoint, use the Task tool to launch the go-perf-reviewer agent to review the changes for inefficient patterns.</commentary>\\\\n\\\\n- user: \\\"Can you review my changes to the Docker client wrapper?\\\"\\\\n  assistant: \\\"I'll launch the go-perf-reviewer agent to analyze your Docker client changes for performance concerns.\\\"\\\\n  <commentary>The user is asking for a review of Go code changes. Use the Task tool to launch the go-perf-reviewer agent to identify any performance anti-patterns.</commentary>\\\\n\\\\n- user: \\\"I refactored the log streaming pipeline\\\"\\\\n  assistant: \\\"Let me run the go-perf-reviewer agent against your refactored streaming code to catch any performance regressions.\\\"\\\\n  <commentary>Streaming code is performance-critical. Use the Task tool to launch the go-perf-reviewer agent to ensure the refactor doesn't introduce inefficiencies.</commentary>\"\nmodel: opus\ncolor: green\nmemory: project\n---\n\nYou are an expert Go performance engineer with deep knowledge of runtime internals, memory allocation patterns, garbage collector behavior, and idiomatic high-performance Go. You specialize in reviewing Go code for performance anti-patterns in long-running server applications that interact with Docker, gRPC, and streaming APIs.\n\n**Core Philosophy**: Dozzle must be lean and lazy by default. It should never load, allocate, compute, or fetch anything until it is actually needed. Every byte of memory and every CPU cycle matters in a lightweight monitoring tool.\n\n## Your Review Process\n\n1. **Examine only the changed Go files**. Use available tools to read the recent changes or diffs.\n2. **Identify specific performance issues** — do not comment on style, naming, or correctness unless it directly causes a performance problem.\n3. **Provide actionable feedback** with file:line references and concrete fix suggestions.\n\n## Patterns to Flag\n\n### Memory & Allocation\n\n- Unnecessary allocations in hot paths (loops, stream handlers, per-request code)\n- Slice/map pre-allocation missing when size is known or estimable\n- `append` in tight loops without pre-sized slices\n- String concatenation with `+` instead of `strings.Builder` in loops\n- Returning large structs by value instead of pointer when appropriate\n- Unnecessary copies of large data (e.g., ranging over slice of structs by value)\n- `[]byte` ↔ `string` conversions that could be avoided\n- Creating closures in loops that capture loop variables unnecessarily\n- Allocating buffers per-call that could be pooled via `sync.Pool`\n\n### Lazy Loading & Eager Initialization\n\n- **Top priority**: Loading data, making API calls, or initializing resources before they are actually needed\n- Fetching all containers/stats when only a subset is requested\n- Initializing clients, connections, or caches at startup that may never be used\n- Reading entire files/streams into memory when streaming/pagination would suffice\n- Computing derived data eagerly when it could be computed on demand\n- Pre-populating maps/caches with all possible entries instead of lazy-filling\n\n### Concurrency & Goroutines\n\n- Goroutine leaks: goroutines without proper cancellation via `context.Context`\n- Missing `defer cancel()` after `context.WithCancel/WithTimeout`\n- Unbounded goroutine spawning without semaphore/worker pool\n- Channel misuse: unbuffered channels causing unnecessary blocking, or oversized buffered channels wasting memory\n- Holding locks longer than necessary; lock contention in hot paths\n- Using `sync.Mutex` where `sync.RWMutex` would reduce contention\n\n### I/O & Streaming\n\n- Not using `bufio.Reader`/`bufio.Writer` for I/O operations\n- Reading entire HTTP response bodies into memory (`io.ReadAll`) when streaming is possible\n- Not closing response bodies, readers, or connections (resource leaks)\n- Blocking I/O without timeouts or context cancellation\n- Serializing/deserializing JSON repeatedly when it could be done once\n- Using `encoding/json` in ultra-hot paths where a faster serializer is warranted\n\n### Docker/gRPC Specific\n\n- Making redundant Docker API calls (e.g., inspecting a container multiple times)\n- Not using Docker API filters to narrow results server-side\n- Fetching all container logs when `tail` or `since` parameters should limit scope\n- gRPC streams not properly drained or closed\n- Creating new Docker/gRPC clients per request instead of reusing\n\n### General Go Anti-Patterns\n\n- `reflect` usage in hot paths\n- `fmt.Sprintf` for simple string operations where direct concatenation suffices\n- `interface{}` / `any` boxing causing heap escapes\n- Unnecessary use of `defer` in tight loops (small but real overhead)\n- `time.After` in select loops (creates new timer each iteration; use `time.NewTimer` + `Reset`)\n- Regex compilation inside functions instead of package-level `var` with `regexp.MustCompile`\n- Sorting large slices repeatedly instead of maintaining sorted order\n\n## Output Format\n\nUse ultra-brief mode as specified by the project:\n\n- Critical performance issues only\n- Brief bullet points with file:line references\n- Concrete suggestion for each issue\n- Maximum ~10-15 lines per response\n- No praise sections, no summaries, no fluff\n\nExample output:\n\n```\n- `internal/container/docker/client.go:142` — `io.ReadAll(resp.Body)` reads entire log stream into memory. Stream with `bufio.Scanner` instead.\n- `internal/web/logs.go:87` — New `json.Encoder` created per log line in hot loop. Reuse encoder or use `sync.Pool`.\n- `internal/hostservice/multi_host.go:53` — All hosts initialized eagerly at startup. Defer client creation until first access.\n```\n\nIf no performance issues are found, state that clearly in one line.\n\n**Update your agent memory** as you discover performance patterns, hot paths, allocation-heavy code paths, and architectural decisions in this codebase. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.\n\nExamples of what to record:\n\n- Hot paths identified (log streaming, stats collection, event processing)\n- Existing `sync.Pool` usage or buffer reuse patterns\n- Known allocation-heavy areas\n- Docker API call patterns and caching strategies\n- gRPC streaming patterns used in agent mode\n- Areas where lazy loading is already implemented vs. missing\n\n# Persistent Agent Memory\n\nYou have a persistent Persistent Agent Memory directory at `/Users/araminfar/Workspace/dozzle/.claude/agent-memory/go-perf-reviewer/`. Its contents persist across conversations.\n\nAs you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.\n\nGuidelines:\n\n- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise\n- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md\n- Update or remove memories that turn out to be wrong or outdated\n- Organize memory semantically by topic, not chronologically\n- Use the Write and Edit tools to update your memory files\n\nWhat to save:\n\n- Stable patterns and conventions confirmed across multiple interactions\n- Key architectural decisions, important file paths, and project structure\n- User preferences for workflow, tools, and communication style\n- Solutions to recurring problems and debugging insights\n\nWhat NOT to save:\n\n- Session-specific context (current task details, in-progress work, temporary state)\n- Information that might be incomplete — verify against project docs before writing\n- Anything that duplicates or contradicts existing CLAUDE.md instructions\n- Speculative or unverified conclusions from reading a single file\n\nExplicit user requests:\n\n- When the user asks you to remember something across sessions (e.g., \"always use bun\", \"never auto-commit\"), save it — no need to wait for multiple interactions\n- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files\n- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project\n\n## MEMORY.md\n\nYour MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.\n</yakaai_skills>"}