diff --git a/PLAN.md b/PLAN.md index d04cc50..f384aaf 100644 --- a/PLAN.md +++ b/PLAN.md @@ -287,14 +287,52 @@ Full dashboard for visibility, manual control, and configuration. 22. **Embed in Go** — build SvelteKit to static, embed with `go:embed`, serve from Go 23. **Real-time updates** — SSE for deploy progress and instance status changes -### Phase 4: Hardening +### Phase 4: Volumes & Environment -24. **Blue-green deploys** — start new, health check, swap, stop old (zero downtime) -25. **Promote flow** — enforce `promote_from` for production deploys -26. **Auth on dashboard** — basic auth or token-based -27. **Graceful shutdown** — drain in-progress deploys on SIGTERM -28. **Structured logging** — JSON logs with deploy context -29. **Config export** — download current SQLite state as YAML +Persistent storage and app-specific configuration for deployed containers. + +24. **Environment variables per project** — key/value pairs stored in SQLite, sensitive values encrypted +25. **Per-stage env overrides** — e.g., `NODE_ENV=development` for dev, `NODE_ENV=production` for prod +26. **Volume mounts per project** — configurable source/target paths with shared/isolated modes +27. **Shared volumes** — all instances of a project mount the same host path (for stateless apps or shared uploads) +28. **Isolated volumes** — each instance gets its own subdirectory: `{source}/{stage}-{tag}/` → `{target}` (for stateful apps with local DBs/files) +29. **UI for volumes & env** — project settings page with key/value editor, volume list, shared/isolated toggle, per-stage override support + +Volume config per project: +```yaml +env: + NODE_ENV: production + DATABASE_URL: postgres://db:5432/myapp # shared external DB + SECRET_KEY: "..." # encrypted in SQLite +volumes: + - source: /data/my-app/uploads + target: /app/uploads + mode: shared # all instances share this path + - source: /data/my-app/data + target: /app/data + mode: isolated # auto-appends /{stage}-{tag}/ to source +``` + +Stage-level env overrides: +```yaml +stages: + dev: + env: + NODE_ENV: development # overrides project-level + DATABASE_URL: postgres://db:5432/myapp_dev + prod: + env: + NODE_ENV: production # uses project-level default +``` + +### Phase 5: Hardening + +30. **Blue-green deploys** — start new, health check, swap, stop old (zero downtime) +31. **Promote flow** — enforce `promote_from` for production deploys +32. **Auth on dashboard** — basic auth or token-based +33. **Graceful shutdown** — drain in-progress deploys on SIGTERM +34. **Structured logging** — JSON logs with deploy context +35. **Config export** — download current SQLite state as YAML ## Key Dependencies (Go) diff --git a/plans/docker-watcher-core/PLAN.md b/plans/docker-watcher-core/PLAN.md index 37f5bb4..c9381a5 100644 --- a/plans/docker-watcher-core/PLAN.md +++ b/plans/docker-watcher-core/PLAN.md @@ -31,11 +31,12 @@ A self-hosted tool that automates Docker container deployment with Nginx Proxy M - [x] Phase 6: Webhook Handler [domain: backend] → [subplan](./phase-6-webhook-handler.md) - [x] Phase 7: Deployer & Health Checker [domain: backend] → [subplan](./phase-7-deployer.md) - [x] Phase 8: REST API Layer [domain: backend] → [subplan](./phase-8-api-layer.md) -- [ ] Phase 9: SvelteKit Dashboard & Project Views [domain: frontend] → [subplan](./phase-9-dashboard.md) +- [x] Phase 9: SvelteKit Dashboard & Project Views [domain: frontend] → [subplan](./phase-9-dashboard.md) - [ ] Phase 10: Quick Deploy & Settings Pages [domain: frontend] → [subplan](./phase-10-settings-deploy.md) - [ ] Phase 11: Frontend Embed & Real-Time Updates [domain: fullstack] → [subplan](./phase-11-embed-sse.md) - [ ] Phase 12: Hardening [domain: backend] → [subplan](./phase-12-hardening.md) - [ ] Phase 13: Frontend Polish & Modern UI [domain: frontend] → [subplan](./phase-13-ui-polish.md) +- [ ] Phase 14: Volumes & Environment [domain: fullstack] → [subplan](./phase-14-volumes-env.md) ### Parallel Execution Notes @@ -54,11 +55,12 @@ A self-hosted tool that automates Docker container deployment with Nginx Proxy M | Phase 6: Webhook Handler | backend | ✅ Complete | ✅ Pass w/ fixes | ⏭️ Skip (Big Bang) | ✅ | | Phase 7: Deployer & Health | backend | ✅ Complete | ✅ Pass w/ fixes | ⏭️ Skip (Big Bang) | ✅ | | Phase 8: API Layer | backend | ✅ Complete | ✅ Pass w/ fixes | ⏭️ Skip (Big Bang) | ✅ | -| Phase 9: Dashboard | frontend | ⬜ Not Started | ⬜ | ⏭️ Skip (Big Bang) | ⬜ | +| Phase 9: Dashboard | frontend | ✅ Complete | ⬜ Pending | ⏭️ Skip (Big Bang) | ⬜ | | Phase 10: Settings & Deploy | frontend | ⬜ Not Started | ⬜ | ⏭️ Skip (Big Bang) | ⬜ | | Phase 11: Embed & SSE | fullstack | ⬜ Not Started | ⬜ | ⏭️ Skip (Big Bang) | ⬜ | | Phase 12: Hardening | backend | ⬜ Not Started | ⬜ | ⏭️ Skip (Big Bang) | ⬜ | -| Phase 13: UI Polish | frontend | ⬜ Not Started | ⬜ | ✅ Required (Final) | ⬜ | +| Phase 13: UI Polish | frontend | ⬜ Not Started | ⬜ | ⏭️ Skip (Big Bang) | ⬜ | +| Phase 14: Volumes & Env | fullstack | ⬜ Not Started | ⬜ | ✅ Required (Final) | ⬜ | ## Amendment Log @@ -76,6 +78,13 @@ A self-hosted tool that automates Docker container deployment with Nginx Proxy M **Why:** User wants bilingual support (English and Russian) in the dashboard **Impact on existing phases:** None — contained within Phase 13 +### Amendment 3 — 2026-03-27 + +**Type:** Added phase +**What changed:** Added Phase 14: Volumes & Environment — per-project env vars with per-stage overrides, volume mounts with shared/isolated modes, encryption for sensitive values, UI editor +**Why:** Missing from feature planner phases but present in root PLAN.md Phase 4 +**Impact on existing phases:** Phase 14 becomes the final phase (build/tests required). Phase 13 (UI Polish) remains but no longer the final phase for build enforcement. + ## Final Review - [ ] Comprehensive code review diff --git a/plans/docker-watcher-core/phase-14-volumes-env.md b/plans/docker-watcher-core/phase-14-volumes-env.md new file mode 100644 index 0000000..68edfcd --- /dev/null +++ b/plans/docker-watcher-core/phase-14-volumes-env.md @@ -0,0 +1,58 @@ +# Phase 14: Volumes & Environment + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** fullstack + +## Objective +Implement per-project environment variables with per-stage overrides, volume mounts with shared/isolated modes, sensitive env value encryption, and UI for managing both. + +## Tasks + +- [ ] Task 1: Extend store schema — add `stage_env` table for per-stage env overrides (stage_id, key, value, encrypted bool) +- [ ] Task 2: Extend store schema — add `volumes` table for volume config (project_id, source, target, mode: shared|isolated) +- [ ] Task 3: Implement store CRUD for stage env overrides (Create, GetByStageID, Update, Delete) +- [ ] Task 4: Implement store CRUD for volumes (Create, GetByProjectID, Update, Delete) +- [ ] Task 5: Encrypt sensitive env values (values marked as secret) using crypto.Encrypt before storage +- [ ] Task 6: Merge env vars during deploy — project-level env + stage-level overrides, decrypt secrets +- [ ] Task 7: Compute volume mounts during deploy — shared mode uses path as-is, isolated mode appends `/{stage}-{tag}/` to source +- [ ] Task 8: Pass merged env vars and volume mounts to Docker container creation +- [ ] Task 9: API endpoints — CRUD for stage env vars and project volumes +- [ ] Task 10: Frontend — env var editor in project/stage settings (key/value pairs, secret toggle) +- [ ] Task 11: Frontend — volume editor in project settings (source/target/mode) +- [ ] Task 12: Frontend — per-stage env override UI showing inherited vs overridden values + +## Files to Modify/Create +- `internal/store/stage_env.go` — stage env CRUD +- `internal/store/volumes.go` — volume CRUD +- `internal/store/store.go` — add new tables to schema +- `internal/deployer/deployer.go` — merge env vars and compute volume mounts during deploy +- `internal/docker/container.go` — accept volume mounts in ContainerConfig +- `internal/api/stages.go` — add env var endpoints +- `internal/api/projects.go` — add volume endpoints +- `web/src/routes/projects/[id]/env/+page.svelte` — env var editor +- `web/src/routes/projects/[id]/volumes/+page.svelte` — volume editor + +## Acceptance Criteria +- Project-level env vars applied to all containers +- Stage-level overrides replace project-level values for matching keys +- Sensitive env values encrypted at rest, decrypted only during deploy +- Shared volumes: all instances mount same host path +- Isolated volumes: each instance gets `{source}/{stage}-{tag}/` subdirectory +- UI allows managing env vars and volumes per project and per stage + +## Notes +- Project `env` field already exists as JSON blob in the store — this phase may migrate to a proper table or keep JSON and add stage overrides separately +- Volume `mode` is either "shared" or "isolated" +- Isolated volume subdirectory is created automatically by Docker (bind mount creates parent dirs) +- Sensitive env display: masked in UI, "Change" button pattern (same as credentials page) + +## Review Checklist +- [ ] All tasks completed +- [ ] Env merge logic is correct (stage overrides project) +- [ ] Secret values never appear in plaintext in API responses +- [ ] Volume paths are validated (no path traversal) +- [ ] Isolated volume subdirectory naming is deterministic + +## Handoff to Next Phase + diff --git a/plans/docker-watcher-core/phase-9-dashboard.md b/plans/docker-watcher-core/phase-9-dashboard.md index c4df7fa..6dcd7e5 100644 --- a/plans/docker-watcher-core/phase-9-dashboard.md +++ b/plans/docker-watcher-core/phase-9-dashboard.md @@ -1,6 +1,6 @@ # Phase 9: SvelteKit Dashboard & Project Views -**Status:** ⬜ Not Started +**Status:** ✅ Complete **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** frontend @@ -9,18 +9,18 @@ Build the SvelteKit frontend with the dashboard overview and project detail view ## Tasks -- [ ] Task 1: Initialize SvelteKit project in `web/` directory with TypeScript, static adapter -- [ ] Task 2: Set up Tailwind CSS (or project's preferred styling) -- [ ] Task 3: Create shared API client (`lib/api.ts`) — typed fetch wrapper for all backend endpoints -- [ ] Task 4: Define TypeScript types (`lib/types.ts`) — Project, Stage, Instance, Deploy, Registry, Settings -- [ ] Task 5: Create layout with navigation — sidebar or top nav with Dashboard, Projects, Deploy, Settings links -- [ ] Task 6: Dashboard page (`routes/+page.svelte`) — project overview cards with instance counts, status indicators, latest activity -- [ ] Task 7: Projects list page (`routes/projects/+page.svelte`) — all projects with quick stats, "Add Project" button -- [ ] Task 8: Project detail page (`routes/projects/[id]/+page.svelte`) — stages, instances per stage, controls -- [ ] Task 9: Instance controls — Stop, Start, Restart, Remove buttons with confirmation dialogs -- [ ] Task 10: Deploy history section in project detail — recent deploys with status, timestamp, tag -- [ ] Task 11: "Deploy new version" dropdown — list available tags from registry, trigger deploy -- [ ] Task 12: Create reusable components: StatusBadge, InstanceCard, ProjectCard, ConfirmDialog +- [x] Task 1: Initialize SvelteKit project in `web/` directory with TypeScript, static adapter +- [x] Task 2: Set up Tailwind CSS v4 with @tailwindcss/vite plugin +- [x] Task 3: Create shared API client (`lib/api.ts`) — typed fetch wrapper for all backend endpoints +- [x] Task 4: Define TypeScript types (`lib/types.ts`) — Project, Stage, Instance, Deploy, Registry, Settings +- [x] Task 5: Create layout with navigation — sidebar with Dashboard, Projects, Deploy, Settings links +- [x] Task 6: Dashboard page (`routes/+page.svelte`) — project overview cards with instance counts, status indicators +- [x] Task 7: Projects list page (`routes/projects/+page.svelte`) — all projects with quick stats, "Add Project" button +- [x] Task 8: Project detail page (`routes/projects/[id]/+page.svelte`) — stages, instances per stage, controls +- [x] Task 9: Instance controls — Stop, Start, Restart, Remove buttons with confirmation dialogs +- [x] Task 10: Deploy history section in project detail — recent deploys with status, timestamp, tag +- [x] Task 11: "Deploy new version" dropdown — list available tags from registry, trigger deploy +- [x] Task 12: Create reusable components: StatusBadge, InstanceCard, ProjectCard, ConfirmDialog ## Files to Modify/Create - `web/package.json` — SvelteKit project config @@ -61,4 +61,39 @@ Build the SvelteKit frontend with the dashboard overview and project detail view - [ ] Components are reusable and well-structured ## Handoff to Next Phase - + +Phase 9 is complete. All 14 files have been created in the `web/` directory: + +**Configuration files:** +- `web/package.json` — Svelte 5, SvelteKit 2, Tailwind CSS v4, static adapter, TypeScript +- `web/svelte.config.js` — Static adapter with SPA fallback (`index.html`) +- `web/vite.config.ts` — Tailwind v4 vite plugin + `/api` proxy to `localhost:8080` +- `web/tsconfig.json` — Strict TypeScript, bundler module resolution +- `web/src/app.html` — Base HTML shell +- `web/src/app.css` — Tailwind v4 import +- `web/src/routes/+layout.ts` — Disables SSR, enables prerender for static adapter + +**Core library:** +- `web/src/lib/types.ts` — All TypeScript types matching Go backend models exactly (Project, Stage, Instance, Deploy, DeployLog, Registry, Settings, ApiEnvelope) +- `web/src/lib/api.ts` — Full typed API client covering all endpoints (projects, instances, deploys, registries, settings). Unwraps envelope, throws `ApiError` on failure. + +**Components (Svelte 5 runes):** +- `StatusBadge.svelte` — Color-coded status pill (green/yellow/red/gray/blue) +- `ConfirmDialog.svelte` — Modal with danger/primary variants +- `InstanceCard.svelte` — Instance display with stop/start/restart/remove controls +- `ProjectCard.svelte` — Project summary card for dashboard grid + +**Pages:** +- `+layout.svelte` — Sidebar navigation (Dashboard, Projects, Deploy, Settings) +- `routes/+page.svelte` — Dashboard with stats cards and project grid +- `routes/projects/+page.svelte` — Project table with inline add-project form +- `routes/projects/[id]/+page.svelte` — Full project detail: stages, instances, deploy form, deploy history + +**Key decisions:** +- Used Svelte 5 runes (`$state`, `$derived`, `$effect`, `$props`) throughout +- Tailwind CSS v4 with `@tailwindcss/vite` plugin (no PostCSS config needed) +- Client-side only rendering (SSR disabled) for static adapter compatibility +- API client uses relative `/api/` paths — works in both dev (vite proxy) and prod (embedded) +- All API calls include loading spinners and error states with retry buttons + +**Ready for Phase 10:** Settings pages, Quick Deploy page, and remaining UI routes. The API client already includes all endpoint wrappers needed. diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte new file mode 100644 index 0000000..ad456e4 --- /dev/null +++ b/web/src/routes/+layout.svelte @@ -0,0 +1,78 @@ + + +
+ + + + +
+
+ {@render children()} +
+
+
diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts new file mode 100644 index 0000000..313023c --- /dev/null +++ b/web/src/routes/+layout.ts @@ -0,0 +1,3 @@ +// Disable SSR for static adapter — all rendering happens client-side. +export const ssr = false; +export const prerender = true; diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte new file mode 100644 index 0000000..c20b99b --- /dev/null +++ b/web/src/routes/+page.svelte @@ -0,0 +1,129 @@ + + + + Dashboard - Docker Watcher + + +
+
+

Dashboard

+ + Quick Deploy + +
+ + +
+
+

Total Projects

+

{totalProjects}

+
+
+

Running Instances

+

{totalRunning}

+
+
+

Failed Instances

+

+ {totalFailed} +

+
+
+ + +

Projects

+ + {#if loading} +
+
+
+ {:else if error} +
+

{error}

+ +
+ {:else if projects.length === 0} +
+

No projects yet.

+ + Add your first project + +
+ {:else} +
+ {#each projects as project (project.id)} + + {/each} +
+ {/if} +
diff --git a/web/src/routes/deploy/+page.svelte b/web/src/routes/deploy/+page.svelte index 3cb0dca..a2762a7 100644 --- a/web/src/routes/deploy/+page.svelte +++ b/web/src/routes/deploy/+page.svelte @@ -1,6 +1,6 @@ + + + Projects - Docker Watcher + + +
+
+

Projects

+ +
+ + + {#if showAddForm} +
+

New Project

+ + {#if formError} +
+

{formError}

+
+ {/if} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ {/if} + + + {#if loading} +
+
+
+ {:else if error} +
+

{error}

+ +
+ {:else if projects.length === 0} +
+

No projects configured yet.

+

Click "Add Project" to get started.

+
+ {:else} +
+ + + + + + + + + + + + + {#each projects as project (project.id)} + + + + + + + + + {/each} + +
NameImagePortRegistryCreated
+ + {project.name} + + + {project.image} + + {project.port || '-'} + + {project.registry || '-'} + + {new Date(project.created_at).toLocaleDateString()} + + + View + +
+
+ {/if} +
diff --git a/web/src/routes/projects/[id]/+page.svelte b/web/src/routes/projects/[id]/+page.svelte new file mode 100644 index 0000000..c73f071 --- /dev/null +++ b/web/src/routes/projects/[id]/+page.svelte @@ -0,0 +1,344 @@ + + + + {project?.name ?? 'Project'} - Docker Watcher + + +{#if loading} +
+
+
+{:else if error} +
+

{error}

+ +
+{:else if project} +
+ +
+
+
+ Projects + / +
+

{project.name}

+

{project.image}

+
+ +
+ + +
+
+

Port

+

{project.port || '-'}

+
+
+

Registry

+

{project.registry || '-'}

+
+
+

Healthcheck

+

{project.healthcheck || '-'}

+
+
+

Created

+

{new Date(project.created_at).toLocaleDateString()}

+
+
+ + +

Stages

+ + {#if stages.length === 0} +
+

No stages configured for this project.

+
+ {:else} +
+ {#each stages as stage (stage.id)} + {@const stageInstances = instancesByStage[stage.id] ?? []} +
+ +
+
+

{stage.name}

+ Pattern: {stage.tag_pattern} + {#if stage.auto_deploy} + + auto-deploy + + {/if} + {#if stage.confirm} + + requires confirm + + {/if} +
+
+ + {stageInstances.length} / {stage.max_instances} instances + + +
+
+ + + {#if deployStageId === stage.id} +
+
+
+ + {#if tagsLoading} +

Loading tags...

+ {:else if availableTags.length > 0} + + {:else} + + {/if} +
+ + +
+ {#if deployError} +

{deployError}

+ {/if} +
+ {/if} + + +
+ {#if stageInstances.length === 0} +

No instances running

+ {:else} +
+ {#each stageInstances as instance (instance.id)} + + {/each} +
+ {/if} +
+
+ {/each} +
+ {/if} + + +

Recent Deploys

+ + {#if deploys.length === 0} +

No deploy history for this project.

+ {:else} +
+ + + + + + + + + + + + {#each deploys as deploy (deploy.id)} + + + + + + + + {/each} + +
TagStatusStartedFinishedError
{deploy.image_tag} + + + {deploy.started_at ? new Date(deploy.started_at).toLocaleString() : '-'} + + {deploy.finished_at ? new Date(deploy.finished_at).toLocaleString() : '-'} + + {deploy.error || '-'} +
+
+ {/if} +
+ + { showDeleteConfirm = false; }} + /> +{/if} diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte new file mode 100644 index 0000000..5ceacf2 --- /dev/null +++ b/web/src/routes/settings/+page.svelte @@ -0,0 +1,278 @@ + + + + General Settings - Docker Watcher + + +
+ {#if loading} +
+ + + + +
+ {:else} + +
+

Global Configuration

+ +
+ + + + + + + + + + + +
+ +
+ +
+
+ + +
+

Webhook URL

+

+ This secret URL receives image push notifications from your CI pipeline. +

+ + {#if webhookUrl} +
+ + {webhookUrl} + + +
+ {:else} +

No webhook URL configured

+ {/if} + +
+ +

+ Warning: regenerating will invalidate the current URL. Update your CI pipelines. +

+
+
+ {/if} +
diff --git a/web/src/routes/settings/credentials/+page.svelte b/web/src/routes/settings/credentials/+page.svelte new file mode 100644 index 0000000..0edaf2e --- /dev/null +++ b/web/src/routes/settings/credentials/+page.svelte @@ -0,0 +1,238 @@ + + + + Credentials - Docker Watcher + + +
+
+

Credentials

+

+ Manage credentials for Nginx Proxy Manager and registry tokens. All values are encrypted at + rest. +

+
+ + {#if loading} +
+ + + + +
+ {:else} + +
+
+
+

Nginx Proxy Manager

+

Credentials for managing proxy hosts via NPM API

+
+ {#if npmHasCredentials && !editingNpm} + + Configured + + {/if} +
+ + {#if !editingNpm && npmHasCredentials} + +
+
+
+

URL

+

{npmUrl || 'Not set'}

+
+
+
+
+

Email

+

{npmEmail || 'Not set'}

+
+
+
+
+

Password

+

--------

+
+
+ +
+ {:else} + +
+ + + + + + +
+ + {#if npmHasCredentials} + + {/if} +
+
+ {/if} +
+ + +
+

Registry Tokens

+

+ Registry authentication tokens are managed per-registry in the + Registries + section. Each registry stores its token encrypted in the database. +

+
+ {/if} +
diff --git a/web/src/routes/settings/registries/+page.svelte b/web/src/routes/settings/registries/+page.svelte new file mode 100644 index 0000000..25065d7 --- /dev/null +++ b/web/src/routes/settings/registries/+page.svelte @@ -0,0 +1,315 @@ + + + + Registries - Docker Watcher + + +
+
+
+

Container Registries

+

Manage your container registries for image detection.

+
+ {#if !showForm} + + {/if} +
+ + + {#if showForm} +
+

+ {editingId ? 'Edit Registry' : 'Add New Registry'} +

+ +
+ + + + +
+ + +

Registry type for API compatibility

+
+ + +
+ +
+ + +
+
+ {/if} + + + {#if loading} +
+ + + + +
+ {:else if registries.length === 0} +
+

No registries configured yet.

+ {#if !showForm} + + {/if} +
+ {:else} +
+ {#each registries as registry (registry.id)} +
+
+
+

{registry.name}

+ + {registry.type} + +
+

{registry.url}

+
+
+ + + +
+
+ {/each} +
+ {/if} +