feat: Cloudflare DNS management with automatic record sync

Add flexible DNS management to Docker Watcher. By default, wildcard DNS
is assumed (current behavior). When disabled, users can configure a
Cloudflare DNS provider with API token and zone selection. DNS A records
are automatically created/updated/deleted in sync with proxy consumers
(deployed instances and standalone proxies).

- Settings: wildcard_dns toggle, dns_provider, cloudflare credentials
- Cloudflare client: Provider interface with EnsureRecord/DeleteRecord/ListRecords
- DNS lifecycle hooks in deployer and proxy manager (best-effort)
- Settings UI: DNS config section with provider picker, zone selector, test button
- DNS Records page at /dns with filtering, sync status, reconciliation
- Records visible in both wildcard and managed modes
- Cleanup on provider change: removes old records when switching modes
This commit is contained in:
2026-04-02 14:49:21 +03:00
parent c9d4895ee3
commit c730cfaa45
46 changed files with 2429 additions and 1260 deletions
@@ -0,0 +1,33 @@
# Feature Context: Cloudflare DNS Management
## Configuration
- **Development mode:** Automated
- **Execution mode:** Direct
- **Strategy:** Big Bang
- **Build (Go):** `go build ./cmd/server`
- **Build (Frontend):** `cd web && npm run build`
- **Check (Frontend):** `cd web && npm run check`
- **Test:** `go test ./...`
- **Dev server:** `./scripts/dev-server.sh` (port 8090)
## Current State
Starting fresh — no implementation yet.
## Cross-Phase Dependencies
- Phase 2 depends on Phase 1 (settings fields for Cloudflare credentials)
- Phase 3 depends on Phase 2 (dns.Provider interface)
- Phase 4 depends on Phase 1 (API endpoints for settings)
- Phase 5 depends on Phase 2 + Phase 6 (client + sync logic)
- Phase 6 depends on Phase 2 (Cloudflare client) + Phase 3 (dns_records table)
## Key Architecture Decisions
- DNS provider abstraction via `internal/dns.Provider` interface
- Cloudflare API v4 via direct HTTP (no SDK) — keeps dependencies minimal
- Local `dns_records` table tracks managed records for reconciliation
- DNS operations are best-effort (log warnings, don't block deploys)
- A records only, pointing to `ServerIP` from settings
## Environment & Runtime Notes
- Encryption key from `ENCRYPTION_KEY` env var (AES-256-GCM)
- SQLite with WAL mode, auto-migration on startup
- Frontend is SvelteKit 2 + Svelte 5 + Tailwind CSS 4
+50
View File
@@ -0,0 +1,50 @@
# Feature: Cloudflare DNS Management
**Branch:** `feature/cloudflare-dns-management`
**Base branch:** `main`
**Created:** 2026-04-02
**Status:** 🟡 In Progress
**Strategy:** Big Bang
**Mode:** Automated
**Execution:** Direct
## Summary
Introduce flexible DNS management. By default, wildcard DNS is assumed (current behavior).
When disabled, the user selects a DNS provider (Cloudflare initially) and provides API
credentials. DNS A records are then automatically kept in sync with proxy consumers
(deployed instances and standalone proxies). A dedicated DNS Records page provides
visibility, filtering, and manual sync/reconciliation.
## Build & Test Commands
- **Build (Go):** `go build ./cmd/server`
- **Build (Frontend):** `cd web && npm run build`
- **Check (Frontend):** `cd web && npm run check`
- **Test (Go):** `go test ./...`
- **Dev server:** `./scripts/dev-server.sh`
## Phases
- [ ] Phase 1: Settings model & API [domain: backend] → [subplan](./phase-1-settings-model.md)
- [ ] Phase 2: Cloudflare DNS client [domain: backend] → [subplan](./phase-2-cloudflare-client.md)
- [ ] Phase 3: DNS lifecycle hooks [domain: backend] → [subplan](./phase-3-dns-hooks.md)
- [ ] Phase 4: Settings UI — DNS configuration [domain: frontend] → [subplan](./phase-4-settings-ui.md)
- [ ] Phase 5: DNS Records page [domain: fullstack] → [subplan](./phase-5-dns-records-page.md)
- [ ] Phase 6: DNS sync & reconciliation [domain: backend] → [subplan](./phase-6-dns-sync.md)
## Phase Progress Log
| Phase | Domain | Status | Review | Build | Committed |
|-------|--------|--------|--------|-------|-----------|
| Phase 1: Settings model & API | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 2: Cloudflare DNS client | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 3: DNS lifecycle hooks | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 4: Settings UI — DNS config | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 5: DNS Records page | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 6: DNS sync & reconciliation | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
## Final Review
- [ ] Comprehensive code review
- [ ] Full build passes
- [ ] Full test suite passes
- [ ] Merged to `main`
@@ -0,0 +1,59 @@
# Phase 1: Settings Model & API
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Extend the Settings model and API to support DNS provider configuration.
## Tasks
- [ ] Task 1: Add new fields to `Settings` struct in `internal/store/models.go`
- `WildcardDNS` (bool, default true)
- `DNSProvider` (string, default "")
- `CloudflareAPIToken` (string, encrypted)
- `CloudflareZoneID` (string)
- [ ] Task 2: Add migration columns in `internal/store/store.go`
- `wildcard_dns` INTEGER DEFAULT 1
- `dns_provider` TEXT DEFAULT ''
- `cloudflare_api_token` TEXT DEFAULT ''
- `cloudflare_zone_id` TEXT DEFAULT ''
- [ ] Task 3: Update `GetSettings()` and `UpdateSettings()` in `internal/store/settings.go`
- Read/write new fields
- Encrypt/decrypt `cloudflare_api_token`
- [ ] Task 4: Update `GET /api/settings` handler to include new fields (mask token)
- [ ] Task 5: Update `PUT /api/settings` handler to accept new fields
- [ ] Task 6: Add `POST /api/settings/dns/test` endpoint — validate Cloudflare token + zone
- [ ] Task 7: Add `GET /api/settings/dns/zones` endpoint — list Cloudflare zones for picker
- [ ] Task 8: Register new routes in `internal/api/router.go`
## Files to Modify/Create
- `internal/store/models.go` — add fields to Settings struct
- `internal/store/store.go` — add migration columns
- `internal/store/settings.go` — update read/write queries
- `internal/api/settings.go` — update handlers, add new endpoints
- `internal/api/router.go` — register new routes
## Acceptance Criteria
- New settings fields are persisted and retrievable
- Cloudflare API token is encrypted at rest
- GET /api/settings returns new fields (token masked)
- PUT /api/settings accepts and stores new fields
- DNS test and zones endpoints registered (can return placeholder until Phase 2)
## Notes
- Token encryption uses existing `crypto.Encrypt/Decrypt`
- `has_cloudflare_api_token` bool in GET response (same pattern as npm_password)
- DNS test/zones endpoints will make real Cloudflare API calls — Phase 2 client needed
for full implementation, but can use inline HTTP calls for these two endpoints
## Review Checklist
- [ ] All tasks completed
- [ ] Code follows project conventions
- [ ] No unintended side effects
- [ ] Build passes
- [ ] Tests pass (new + existing)
## Handoff to Next Phase
<!-- Filled in after completion -->
@@ -0,0 +1,61 @@
# Phase 2: Cloudflare DNS Client
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Create an `internal/dns` package with a `Provider` interface and a Cloudflare implementation
using the Cloudflare API v4 (direct HTTP, no SDK).
## Tasks
- [ ] Task 1: Define `Provider` interface in `internal/dns/provider.go`
- `EnsureRecord(ctx, fqdn, ip) error` — create or update A record
- `DeleteRecord(ctx, fqdn) error` — delete A record if exists
- `ListRecords(ctx) ([]Record, error)` — list all A records in the zone
- `Record` struct: ID, FQDN, Type, Content (IP), Proxied, TTL
- [ ] Task 2: Create `internal/dns/cloudflare.go` — Cloudflare implementation
- HTTP client with `Authorization: Bearer <token>` header
- Base URL: `https://api.cloudflare.com/client/v4`
- `EnsureRecord`: GET records by name, create if missing, update if IP differs
- `DeleteRecord`: GET record by name, DELETE if found
- `ListRecords`: GET all A records in zone
- `ListZones`: GET zones for the token (for zone picker)
- `TestConnection`: verify token works (GET /user/tokens/verify)
- [ ] Task 3: Create `internal/dns/dns.go` — factory function
- `NewProvider(providerName, config) (Provider, error)`
- Config struct with token, zoneID
- Returns `nil, nil` when providerName is empty (wildcard mode)
- [ ] Task 4: Wire DNS test/zones endpoints in `internal/api/settings.go`
- `POST /api/settings/dns/test` — create temp Cloudflare client, call TestConnection
- `GET /api/settings/dns/zones` — create temp client, call ListZones
## Files to Modify/Create
- `internal/dns/provider.go` — interface + Record type
- `internal/dns/cloudflare.go` — Cloudflare implementation
- `internal/dns/dns.go` — factory function
- `internal/api/settings.go` — wire test/zones endpoints to real client
## Acceptance Criteria
- Provider interface defined with EnsureRecord, DeleteRecord, ListRecords
- Cloudflare client makes correct API calls with proper auth headers
- EnsureRecord is idempotent (create if missing, update if changed, no-op if same)
- DeleteRecord is idempotent (no error if record doesn't exist)
- ListZones returns zone ID + name pairs
- TestConnection returns success/failure
## Notes
- Cloudflare API v4 docs: zones endpoint, dns_records endpoint
- Use `context.Context` for timeout control on all HTTP calls
- A records only (type "A"), TTL=1 (auto), proxied=false (DNS only, not CF proxy)
## Review Checklist
- [ ] All tasks completed
- [ ] Code follows project conventions
- [ ] No unintended side effects
- [ ] Build passes
- [ ] Tests pass (new + existing)
## Handoff to Next Phase
<!-- Filled in after completion -->
@@ -0,0 +1,67 @@
# Phase 3: DNS Lifecycle Hooks
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Hook DNS record management into the deployer and standalone proxy manager so that DNS
records are automatically created/updated/deleted in sync with proxy consumers.
## Tasks
- [ ] Task 1: Create `dns_records` table for tracking managed records
- Columns: id, fqdn, provider_record_id, consumer_type (instance/standalone), consumer_id, created_at, updated_at
- Store queries: CreateDNSRecord, DeleteDNSRecord, GetDNSRecordByFQDN, ListDNSRecords, GetDNSRecordsByConsumer
- [ ] Task 2: Add DNS provider to `Deployer` struct
- Accept `dns.Provider` in constructor (can be nil for wildcard mode)
- Helper: `ensureDNS(ctx, fqdn, deployID)` — calls provider.EnsureRecord + saves to dns_records
- Helper: `removeDNS(ctx, fqdn, deployID)` — calls provider.DeleteRecord + removes from dns_records
- [ ] Task 3: Hook into deployer — instance creation
- After `configureProxy` succeeds in `deployer.go` and `bluegreen.go` → call `ensureDNS`
- FQDN = `subdomain + "." + settings.Domain`
- [ ] Task 4: Hook into deployer — instance removal
- In `removeInstance` after NPM proxy deletion → call `removeDNS`
- In `rollback` after NPM proxy deletion → call `removeDNS`
- [ ] Task 5: Hook into standalone proxy manager
- `CreateProxy` → after NPM host created, call `ensureDNS`
- `UpdateProxy` → if domain changed, `removeDNS(old)` + `ensureDNS(new)`
- `DeleteProxy` → call `removeDNS`
- [ ] Task 6: Wire DNS provider into main.go
- Read settings on startup, create provider if non-wildcard
- Pass provider to Deployer and proxy Manager constructors
- Handle provider being nil (wildcard mode = no DNS ops)
- [ ] Task 7: Add `DNSRecord` model to `internal/store/models.go`
## Files to Modify/Create
- `internal/store/models.go` — add DNSRecord struct
- `internal/store/store.go` — add dns_records table migration
- `internal/store/dns_records.go` — CRUD queries
- `internal/deployer/deployer.go` — add DNS hooks
- `internal/deployer/bluegreen.go` — add DNS hooks
- `internal/deployer/rollback.go` — add DNS cleanup
- `internal/proxy/manager.go` — add DNS hooks
- `cmd/server/main.go` — wire DNS provider
## Acceptance Criteria
- DNS records created when proxy consumers are created (if non-wildcard mode)
- DNS records deleted when proxy consumers are removed
- DNS records updated when standalone proxy domain changes
- All DNS operations are best-effort (log warning on failure, don't block)
- dns_records table tracks all managed records
- Wildcard mode (default) skips all DNS operations
## Notes
- DNS operations must be wrapped in error handling that logs but doesn't fail the deploy
- The dns_records table is the local source of truth for reconciliation (Phase 6)
- Provider can be nil — all hooks must check for nil before calling
## Review Checklist
- [ ] All tasks completed
- [ ] Code follows project conventions
- [ ] No unintended side effects
- [ ] Build passes
- [ ] Tests pass (new + existing)
## Handoff to Next Phase
<!-- Filled in after completion -->
@@ -0,0 +1,55 @@
# Phase 4: Settings UI — DNS Configuration
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Add a "DNS Configuration" section to the Settings page with wildcard toggle, provider
selection, Cloudflare credential fields, and connection test.
## Tasks
- [ ] Task 1: Add new API functions in `web/src/lib/api.ts`
- `testDnsConnection(token, zoneId)` → POST /api/settings/dns/test
- `listDnsZones(token)` → GET /api/settings/dns/zones
- [ ] Task 2: Add i18n keys for DNS settings in locale files
- [ ] Task 3: Add DNS Configuration section to `web/src/routes/settings/+page.svelte`
- Toggle: "Wildcard DNS is configured" (checkbox/switch)
- When unchecked, show:
- DNS Provider dropdown (only "Cloudflare" option)
- API Token field (password type, show `has_cloudflare_api_token` indicator)
- Zone picker (loaded from API after token provided)
- "Test Connection" button with success/error feedback
- All DNS fields hidden when wildcard is checked
- [ ] Task 4: Wire save logic — include new fields in `handleSave`
- [ ] Task 5: Wire load logic — populate DNS fields from settings response
## Files to Modify/Create
- `web/src/lib/api.ts` — add DNS API functions
- `web/src/routes/settings/+page.svelte` — add DNS config section
- `web/src/lib/i18n/en.ts` (or equivalent locale file) — add DNS translation keys
## Acceptance Criteria
- Wildcard toggle visible and functional (default: checked)
- Unchecking reveals Cloudflare configuration form
- API token field uses password masking
- Zone picker loads zones from Cloudflare API
- Test Connection button shows success/failure
- Settings save includes DNS fields
- Settings load populates DNS fields
## Notes
- Follow existing settings page patterns (FormField, EntityPicker for zones)
- Zone picker similar to SSL certificate picker pattern
- Token field similar to NPM password field (has_token indicator)
## Review Checklist
- [ ] All tasks completed
- [ ] Code follows project conventions
- [ ] No unintended side effects
- [ ] Build passes
- [ ] Tests pass (new + existing)
## Handoff to Next Phase
<!-- Filled in after completion -->
@@ -0,0 +1,65 @@
# Phase 5: DNS Records Page
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
## Objective
Create a dedicated DNS Records page that lists all managed DNS records with filtering,
consumer mapping, and sync status visibility.
## Tasks
- [ ] Task 1: Add backend endpoint `GET /api/dns/records`
- Returns merged view: local dns_records + Cloudflare actual records
- Each record: fqdn, type, value (IP), consumer_type, consumer_name, status (synced/orphaned/missing)
- Orphaned = exists in Cloudflare but no local consumer
- Missing = local consumer exists but no Cloudflare record
- [ ] Task 2: Add API handler in `internal/api/dns.go`
- New handler file for DNS-related endpoints
- Register routes in router.go
- [ ] Task 3: Add frontend API function `getDnsRecords()` in `api.ts`
- [ ] Task 4: Create DNS Records page at `web/src/routes/dns/+page.svelte`
- Table with columns: FQDN, Type, Value, Consumer, Status
- Consumer column shows: instance name (project/stage) or standalone proxy name
- Status badges: synced (green), orphaned (yellow), missing (red)
- Search filter (by FQDN substring)
- Filter by consumer type: all / managed / standalone
- Filter by status: all / synced / orphaned / missing
- Manual sync button (calls POST /api/dns/sync — Phase 6)
- Refresh button to re-fetch from Cloudflare
- [ ] Task 5: Add navigation link to DNS page
- Only visible when wildcard DNS is disabled
- Add to sidebar/nav under Settings or as top-level
- [ ] Task 6: Add i18n keys for DNS records page
## Files to Modify/Create
- `internal/api/dns.go` — new handler file
- `internal/api/router.go` — register DNS routes
- `web/src/lib/api.ts` — add DNS records API function
- `web/src/routes/dns/+page.svelte` — new page
- `web/src/routes/dns/+page.ts` — optional load function
- Navigation component — add DNS link
- Locale files — add i18n keys
## Acceptance Criteria
- DNS Records page accessible at /dns
- Table shows all records with correct status
- Filtering works: search text, consumer type, sync status
- Only accessible/visible when wildcard DNS is disabled
- Consumer names resolve correctly (project/stage for managed, proxy name for standalone)
## Notes
- Status computation: compare local dns_records table with Cloudflare ListRecords response
- Cache Cloudflare response for a few seconds to avoid rate limiting on page load
- Navigation link visibility tied to settings (may need a store or settings check)
## Review Checklist
- [ ] All tasks completed
- [ ] Code follows project conventions
- [ ] No unintended side effects
- [ ] Build passes
- [ ] Tests pass (new + existing)
## Handoff to Next Phase
<!-- Filled in after completion -->
@@ -0,0 +1,62 @@
# Phase 6: DNS Sync & Reconciliation
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Implement reconciliation logic that compares expected DNS records (from active consumers)
with actual Cloudflare records, and provides a sync endpoint to fix discrepancies.
## Tasks
- [ ] Task 1: Add `POST /api/dns/sync` endpoint
- Computes expected records from: active instances with proxy + standalone proxies
- Fetches actual records from Cloudflare via ListRecords
- Creates missing records (consumer exists, no CF record)
- Deletes orphaned records (CF record exists, no consumer) — only for records in dns_records table
- Updates dns_records table to reflect current state
- Returns sync report: created N, deleted N, already_synced N
- [ ] Task 2: Add helper to compute expected records
- Query all instances where npm_proxy_id > 0 and status = "running" → extract FQDN
- Query all standalone proxies → extract domain
- Return list of expected FQDNs
- [ ] Task 3: Add `DELETE /api/dns/records/{fqdn}` endpoint
- Manual deletion of a specific DNS record (for orphan cleanup)
- Calls provider.DeleteRecord + removes from dns_records
- [ ] Task 4: Wire sync endpoint in `internal/api/dns.go` and router
- [ ] Task 5: Add frontend sync button handler in DNS Records page
- Call POST /api/dns/sync
- Show sync report (toast or inline)
- Refresh records list after sync
## Files to Modify/Create
- `internal/api/dns.go` — add sync + delete endpoints
- `internal/api/router.go` — register new routes
- `internal/store/dns_records.go` — add helper queries (list consumers with FQDNs)
- `web/src/lib/api.ts` — add syncDnsRecords(), deleteDnsRecord() functions
- `web/src/routes/dns/+page.svelte` — wire sync button
## Acceptance Criteria
- POST /api/dns/sync creates missing and removes orphaned records
- Sync report returned with counts
- Manual delete endpoint works for individual records
- Frontend sync button triggers reconciliation and refreshes view
- Only records tracked in dns_records table are candidates for orphan deletion
(don't delete unrelated Cloudflare records)
## Notes
- Safety: only delete Cloudflare records that are tracked in our dns_records table
(never touch records we didn't create)
- Rate limiting: Cloudflare API has rate limits, batch operations where possible
- Expected records query needs to join instances + standalone_proxies with settings.domain
## Review Checklist
- [ ] All tasks completed
- [ ] Code follows project conventions
- [ ] No unintended side effects
- [ ] Build passes
- [ ] Tests pass (new + existing)
## Handoff to Next Phase
<!-- Filled in after completion -->
-53
View File
@@ -1,53 +0,0 @@
# Feature Context: Docker Watcher Core
## Configuration
- **Development mode:** Automated
- **Execution mode:** Orchestrator
- **Strategy:** Big Bang (with per-phase code quality reviews)
- **Build (Go):** `go build ./cmd/server/`
- **Test (Go):** `go test ./...`
- **Lint (Go):** `golangci-lint run`
- **Build (Frontend):** `cd web && npm run build`
- **Test (Frontend):** `cd web && npm test`
- **Dev server:** `go run ./cmd/server/`
## Current State
Greenfield project. Only PLAN.md exists with the architecture document.
## Temporary Workarounds
None yet.
## Cross-Phase Dependencies
- Phase 2 depends on Phase 1 (store CRUD for seed import)
- Phases 3 and 4 are independent of each other (can run in parallel)
- Phase 5 depends on Phase 1 (store for poll state)
- Phase 6 depends on Phase 3 (Docker inspect for auto-creation) and Phase 1 (store)
- Phase 7 depends on Phases 3, 4, 5 (Docker, NPM, registry clients)
- Phase 8 depends on Phases 1-7 (wires everything to HTTP)
- Phases 9 and 10 are independent of each other (can run in parallel)
- Phase 11 depends on Phases 8, 9, 10 (embeds frontend, SSE wires to API)
- Phase 12 depends on all prior phases
## Deferred Work
None yet.
## Failed Approaches
None yet.
## Review Findings Log
None yet.
## Phase Execution Log
| Phase | Agent Used | Test Writer | Parallel | Notes |
|-------|-----------|-------------|----------|-------|
| — | — | — | — | No phases executed yet |
## Environment & Runtime Notes
- Platform: Windows 10 (development), Linux (deployment target)
- Docker socket: `/var/run/docker.sock` (Linux) — development may need Docker Desktop
- Go version: TBD (will be determined in Phase 1)
## Implementation Notes
- Big Bang strategy: intermediate phases skip build/tests, code quality reviews after every phase
- Final phase (12) is the only phase where build + full test suite must pass
- Phases 3+4 and 9+10 identified for parallel execution
-108
View File
@@ -1,108 +0,0 @@
# Feature: Docker Watcher Core
**Branch:** `feature/docker-watcher-core`
**Base branch:** `main`
**Created:** 2026-03-27
**Status:** 🟡 In Progress
**Strategy:** Big Bang (with per-phase code quality reviews)
**Mode:** Automated
**Execution:** Orchestrator
## Summary
A self-hosted tool that automates Docker container deployment with Nginx Proxy Manager integration. Detects new images from Gitea/GitHub registries, deploys containers, and configures reverse proxy routing — all from a web dashboard. Supports multiple simultaneous versions of the same project.
## Build & Test Commands
- **Build (Go):** `go build ./cmd/server/`
- **Test (Go):** `go test ./...`
- **Lint (Go):** `golangci-lint run`
- **Build (Frontend):** `cd web && npm run build`
- **Test (Frontend):** `cd web && npm test`
- **Dev server:** `go run ./cmd/server/`
## Phases
- [x] Phase 1: Project Scaffold & SQLite Store [domain: backend] → [subplan](./phase-1-scaffold-store.md)
- [x] Phase 2: Crypto & Config Seed Loader [domain: backend] → [subplan](./phase-2-crypto-config.md)
- [x] Phase 3: Docker Client [domain: backend] → [subplan](./phase-3-docker-client.md)
- [x] Phase 4: NPM Client [domain: backend] → [subplan](./phase-4-npm-client.md)
- [x] Phase 5: Registry Client & Poller [domain: backend] → [subplan](./phase-5-registry-poller.md)
- [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)
- [x] Phase 9: SvelteKit Dashboard & Project Views [domain: frontend] → [subplan](./phase-9-dashboard.md)
- [x] Phase 10: Quick Deploy & Settings Pages [domain: frontend] → [subplan](./phase-10-settings-deploy.md)
- [x] Phase 11: Frontend Embed & Real-Time Updates [domain: fullstack] → [subplan](./phase-11-embed-sse.md)
- [x] Phase 12: Hardening [domain: backend] → [subplan](./phase-12-hardening.md)
- [x] Phase 13: Volumes & Environment [domain: fullstack] → [subplan](./phase-14-volumes-env.md)
- [x] Phase 14: Frontend Polish & Modern UI [domain: frontend] → [subplan](./phase-13-ui-polish.md)
### Parallel Execution Notes
- Phases 3 and 4 are independent (Docker client vs NPM client) — can run in parallel
- Phases 9 and 10 are independent (dashboard vs settings pages) — can run in parallel
## Phase Progress Log
| Phase | Domain | Status | Review | Build | Committed |
| ----- | ------ | ------ | ------ | ----- | --------- |
| Phase 1: Scaffold & Store | backend | ✅ Complete | ✅ Pass w/ fixes | ⏭️ Skip (Big Bang) | ✅ |
| Phase 2: Crypto & Config | backend | ✅ Complete | ✅ Pass w/ notes | ⏭️ Skip (Big Bang) | ✅ |
| Phase 3: Docker Client | backend | ✅ Complete | ✅ Pass w/ fixes | ⏭️ Skip (Big Bang) | ✅ |
| Phase 4: NPM Client | backend | ✅ Complete | ✅ Pass w/ fixes | ⏭️ Skip (Big Bang) | ✅ |
| Phase 5: Registry & Poller | backend | ✅ Complete | ✅ Pass w/ fixes | ⏭️ Skip (Big Bang) | ✅ |
| 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 | ✅ Complete | ⬜ Pending | ⏭️ Skip (Big Bang) | ✅ |
| Phase 10: Settings & Deploy | frontend | ✅ Complete | ⬜ Pending | ⏭️ Skip (Big Bang) | ✅ |
| Phase 11: Embed & SSE | fullstack | ✅ Complete | ⬜ Pending | ⏭️ Skip (Big Bang) | ✅ |
| Phase 12: Hardening | backend | ✅ Complete | ⬜ Pending | ⏭️ Skip (Big Bang) | ✅ |
| Phase 13: Volumes & Env | fullstack | ✅ Complete | ⬜ Pending | ⏭️ Skip (Big Bang) | ✅ |
| Phase 14: UI Polish | frontend | ✅ Complete | ⬜ Pending | ✅ Required (Final) | ⬜ |
## Amendment Log
### Amendment 1 — 2026-03-27
**Type:** Added phase
**What changed:** Added Phase 13: Frontend Polish & Modern UI after Phase 12
**Why:** User wants modern look & feel with SVG icons and polished frontend
**Impact on existing phases:** None — Phase 13 runs after all functionality is complete. Build/tests now required on Phase 13 (final) instead of Phase 12.
### Amendment 2 — 2026-03-27
**Type:** Modified phase
**What changed:** Added Task 13 (EN/RU localization) to Phase 13: Frontend Polish & Modern UI
**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.
### Amendment 4 — 2026-03-27
**Type:** Modified phase
**What changed:** Updated Phase 12 (Hardening) auth tasks to support two modes: Local auth (username/password in SQLite with bcrypt) and OAuth2/OIDC (Authentik or any OIDC provider with configurable discovery URL). Added auth settings UI, user management, OIDC callback flow.
**Why:** Root PLAN.md was updated to require OAuth2/OIDC support alongside local auth
**Impact on existing phases:** Phase 12 task count increased from 10 to 12. Added new files for auth module and login page.
### Amendment 5 — 2026-03-27
**Type:** Reordered phases
**What changed:** Swapped Phase 13 (UI Polish) and Phase 14 (Volumes & Env). Volumes & Env is now Phase 13, UI Polish is now Phase 14 (final).
**Why:** Volumes & Env adds new UI pages that need the polish pass. UI Polish must run last to cover all pages including auth (Phase 12) and volume/env editors (Phase 13).
**Impact on existing phases:** Execution order changed. UI Polish (now Phase 14) remains the final phase with build/test enforcement.
## Final Review
- [ ] Comprehensive code review
- [ ] Full build passes
- [ ] Full test suite passes
- [ ] Security review
- [ ] Merged to `main`
@@ -1,95 +0,0 @@
# Phase 1: Project Scaffold & SQLite Store
**Status:** ✅ Complete
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Initialize the Go project, establish the directory structure, and implement the SQLite store with schema, migrations, and CRUD operations for all entities.
## Tasks
- [x] Task 1: Initialize Go module (`go mod init`), create directory structure per PLAN.md
- [x] Task 2: Add core dependencies to go.mod (sqlite, chi, yaml, uuid, cron)
- [x] Task 3: Define SQLite schema — tables for projects, stages, registries, settings, instances, deploys, deploy_logs
- [x] Task 4: Implement store initialization with auto-migration (create tables if not exist)
- [x] Task 5: Implement projects CRUD (Create, GetByID, GetAll, Update, Delete)
- [x] Task 6: Implement stages CRUD (Create, GetByProjectID, Update, Delete)
- [x] Task 7: Implement registries CRUD (Create, GetByID, GetAll, Update, Delete)
- [x] Task 8: Implement settings Get/Update (single-row config pattern)
- [x] Task 9: Implement instances CRUD (Create, GetByStageID, GetByID, Update, Delete, UpdateStatus)
- [x] Task 10: Implement deploys CRUD (Create, GetByProjectID, GetRecent, GetByID) + deploy_logs append
- [x] Task 11: Create `cmd/server/main.go` entry point (minimal — just opens DB, defers close)
## Files to Modify/Create
- `go.mod` — module definition and dependencies
- `go.sum` — dependency checksums
- `cmd/server/main.go` — entry point
- `internal/store/store.go` — DB connection, schema, migrations
- `internal/store/projects.go` — project queries
- `internal/store/stages.go` — stage queries
- `internal/store/registries.go` — registry queries
- `internal/store/settings.go` — settings queries
- `internal/store/instances.go` — instance queries
- `internal/store/deploys.go` — deploy history queries
## Acceptance Criteria
- `go mod tidy` succeeds
- All store CRUD functions are implemented with proper error handling
- Schema covers all entities from the architecture plan
- Entry point compiles (may not fully run until later phases wire everything)
## Notes
- Use `modernc.org/sqlite` for CGo-free SQLite
- Use `go-chi/chi/v5` for routing (will be wired in Phase 8)
- Settings table uses a single-row pattern (one row, upsert on update)
- Instance status should be an enum-like string: "running", "stopped", "failed", "removing"
- Deploy status: "pending", "pulling", "starting", "configuring_proxy", "health_checking", "success", "failed", "rolled_back"
## Review Checklist
- [ ] All tasks completed
- [ ] Code follows Go conventions (gofmt, proper error returns)
- [ ] No unintended side effects
- [ ] Schema is normalized and covers all planned entities
- [ ] CRUD functions handle not-found cases properly
## Handoff to Next Phase
### What was built
- Go module initialized at `github.com/alexei/docker-watcher` with all core dependencies
- Full directory structure created: `cmd/server/`, `internal/store/`, plus empty dirs for config, docker, npm, registry, deployer, health, notify, webhook, api, crypto
- SQLite store with 7 tables: projects, stages, registries, settings, instances, deploys, deploy_logs
- Auto-migration runs on store initialization (CREATE TABLE IF NOT EXISTS)
- WAL mode, foreign keys, and busy timeout pragmas enabled
- Settings table uses single-row pattern with `INSERT OR IGNORE` seed
- Models extracted to `internal/store/models.go` for clean separation
### Key files
- `go.mod` — module definition with modernc.org/sqlite, chi, yaml, uuid, cron
- `cmd/server/main.go` — entry point that creates data dir, opens store, defers close
- `internal/store/store.go` — DB connection, pragmas, schema DDL, migration
- `internal/store/models.go` — all entity structs (Project, Stage, Registry, Settings, Instance, Deploy, DeployLog)
- `internal/store/projects.go` — full CRUD
- `internal/store/stages.go` — full CRUD with bool-to-int conversion for SQLite
- `internal/store/registries.go` — full CRUD
- `internal/store/settings.go` — Get/Update (single-row upsert)
- `internal/store/instances.go` — full CRUD + UpdateStatus
- `internal/store/deploys.go` — Create, GetByID, GetByProjectID, GetRecent, UpdateDeployStatus, SetDeployInstanceID, AppendDeployLog, GetDeployLogs
### Conventions established
- UUIDs generated via `github.com/google/uuid` on Create operations
- Timestamps stored as `datetime('now')` defaults in schema, `time.Now().UTC().Format("2006-01-02 15:04:05")` in Go code
- All query errors wrapped with `fmt.Errorf` and `%w` for unwrapping
- Not-found cases return descriptive error strings (not sentinel errors yet — can be refined)
- Boolean fields stored as INTEGER (0/1) in SQLite, converted via `boolToInt` helper
- JSON-encoded maps stored as TEXT for env and volumes fields
### What Phase 2 needs to know
- `store.New(dbPath)` returns a `*Store` that is ready to use — no additional init needed
- The `settings` table is pre-seeded with a row (id=1) so `GetSettings` always works
- Registry `token` and settings `npm_password` are stored as plain text — Phase 2 (Crypto) should add encryption/decryption around these fields
- `go.sum` does not exist yet — run `go mod tidy` after Go is available to generate it
@@ -1,56 +0,0 @@
# Phase 10: Quick Deploy & Settings Pages
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Build the Quick Deploy page (paste image, auto-inspect, one-click deploy) and all Settings pages (registries, credentials, global settings, webhook URL).
## Tasks
- [ ] Task 1: Quick Deploy page (`routes/deploy/+page.svelte`) — image URL input, inspect button
- [ ] Task 2: Quick Deploy inspect flow — call /api/deploy/inspect, display auto-filled form (project name, port, stage, subdomain)
- [ ] Task 3: Quick Deploy submit — user reviews defaults, clicks Deploy, calls /api/deploy/quick
- [ ] Task 4: Settings layout (`routes/settings/+layout.svelte`) — sub-navigation for settings sections
- [ ] Task 5: Global settings page (`routes/settings/+page.svelte`) — domain, server IP, network, subdomain pattern, polling interval
- [ ] Task 6: Registries page (`routes/settings/registries/+page.svelte`) — list, add, edit, delete, test connection
- [ ] Task 7: Credentials page (`routes/settings/credentials/+page.svelte`) — NPM credentials, registry tokens (masked display)
- [ ] Task 8: Webhook URL display and regenerate button in settings
- [ ] Task 9: Projects config page (`routes/projects/config/+page.svelte` or integrated into project detail) — add/edit/delete projects, configure stages
- [ ] Task 10: Stage configuration form — tag patterns, auto_deploy toggle, max_instances, subdomain override
- [ ] Task 11: Form validation on all input pages — required fields, URL format, port range
- [ ] Task 12: Success/error toast notifications for all form submissions
## Files to Modify/Create
- `web/src/routes/deploy/+page.svelte` — quick deploy
- `web/src/routes/settings/+layout.svelte` — settings layout
- `web/src/routes/settings/+page.svelte` — global settings
- `web/src/routes/settings/registries/+page.svelte` — registry management
- `web/src/routes/settings/credentials/+page.svelte` — credential management
- `web/src/lib/components/Toast.svelte` — toast notifications
- `web/src/lib/components/FormField.svelte` — reusable form field with validation
## Acceptance Criteria
- Quick Deploy: paste image URL → inspect → review defaults → deploy works end-to-end
- All settings are editable and saved via API
- Registry test connection shows success/failure
- Credentials are masked in display (`••••••••`)
- Webhook URL is shown with copy button and regenerate option
- Form validation prevents bad submissions
## Notes
- Quick Deploy is the zero-config entry point — should be dead simple UX
- Credential fields: show mask, edit replaces entirely (no partial edit)
- Registry test: calls POST /api/registries/:id/test, shows connection result
- Toast component: appears top-right, auto-dismiss after 5s, color-coded (green/red)
## Review Checklist
- [ ] All tasks completed
- [ ] Quick deploy flow is intuitive (minimal clicks)
- [ ] Credentials never shown in plaintext in UI
- [ ] Form validation covers required fields and formats
- [ ] Error states are handled with user-friendly messages
## Handoff to Next Phase
<!-- Filled in by the implementation agent after completing this phase. -->
@@ -1,76 +0,0 @@
# Phase 11: Frontend Embed & Real-Time Updates
**Status:** Done
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
## Objective
Build SvelteKit to static files, embed into the Go binary with `go:embed`, serve from Go, and implement SSE for real-time deploy progress and instance status updates.
## Tasks
- [x] Task 1: Configure SvelteKit static adapter to output to `web/build/` (already configured)
- [x] Task 2: Add `//go:embed web/build` directive in Go — `web.go` at project root
- [x] Task 3: Create Go handler for serving embedded SPA — `internal/api/static.go` with SPA fallback
- [x] Task 4: Implement SSE endpoint for deploy logs — `GET /api/deploys/:id/logs` (SSE + JSON fallback)
- [x] Task 5: Implement SSE endpoint for instance status — `GET /api/events` streams instance status changes
- [x] Task 6: Create event bus/broadcaster in Go — `internal/events/bus.go` with pub/sub channels
- [x] Task 7: Frontend: connect to SSE for deploy progress — `connectDeployLogs()` in `web/src/lib/sse.ts`
- [x] Task 8: Frontend: connect to SSE for instance status — global SSE in `+layout.svelte` via store
- [x] Task 9: Handle SSE reconnection in frontend — exponential backoff with jitter in `connectSSE()`
- [x] Task 10: Build script/Makefile — `make build` builds frontend then Go binary
## Files to Modify/Create
- `web/svelte.config.js` — already configured with static adapter outputting to `web/build/`
- `web.go` — root-level embed directive (`//go:embed web/build`)
- `internal/api/static.go` — embedded static file server with SPA fallback
- `internal/api/sse.go` — SSE endpoints for deploy logs and instance events
- `internal/events/bus.go` — event bus for publishing/subscribing to events
- `web/src/lib/sse.ts` — SSE client helper with auto-reconnect
- `web/src/lib/stores/instance-status.ts` — Svelte store for real-time instance status
- `web/src/routes/+layout.svelte` — wired up global SSE connection for instance status
- `Makefile` — build frontend + backend
- `cmd/server/main.go` — wired embedded static serving and event bus
- `internal/api/router.go` — added eventBus to Server, SSE routes
- `internal/api/deploys.go` — removed old JSON stub, replaced by SSE handler
- `internal/deployer/deployer.go` — added event publishing for deploy logs, status, instance status
## Acceptance Criteria
- `make build` produces a single Go binary with embedded frontend
- Go binary serves the SvelteKit SPA on all non-API routes
- Deploy progress streams in real-time via SSE
- Instance status updates appear without page refresh
- SSE reconnects automatically after network hiccups
## Notes
- `go:embed` requires the embedded directory to be relative to the Go source file
- SPA fallback: any request that doesn't match `/api/*` gets `index.html`
- Event bus: simple pub/sub with channels — no external dependency needed
- SSE format: `data: {"type": "deploy_log", "payload": {...}}\n\n`
- Keep SSE connections lightweight — use context cancellation for cleanup
- WriteTimeout on HTTP server set to 0 to support long-lived SSE connections
- Deploy logs endpoint serves both SSE (Accept: text/event-stream) and JSON (default)
## Review Checklist
- [x] All tasks completed
- [x] Single binary serves both API and frontend
- [x] SSE handles multiple concurrent clients (buffered channels, non-blocking publish)
- [x] No goroutine leaks on SSE disconnect (context cancellation + Unsubscribe)
- [x] Build process is reproducible (Makefile)
## Handoff to Next Phase
### What was implemented
- **Event bus** (`internal/events/bus.go`): In-process pub/sub with topic filtering, buffered subscriber channels (64 events), non-blocking publish. Supports `EventDeployLog`, `EventInstanceStatus`, and `EventDeployStatus` event types.
- **SSE endpoints**: `GET /api/deploys/{id}/logs` streams deploy logs with JSON fallback; `GET /api/events` streams global instance/deploy status changes.
- **Static file serving**: `web.go` at project root embeds `web/build/`, `internal/api/static.go` serves SPA with fallback. Mounted via chi's `NotFound` handler.
- **Frontend SSE client** (`web/src/lib/sse.ts`): `connectSSE()` with exponential backoff + jitter, `connectDeployLogs()` and `connectGlobalEvents()` convenience functions.
- **Instance status store** (`web/src/lib/stores/instance-status.ts`): Svelte writable store updated by global SSE connection in `+layout.svelte`.
- **Deployer integration**: `deployer.go` now publishes deploy log, deploy status, and instance status events via `EventPublisher` interface.
### Key integration points for next phase
- `events.Bus` is passed to both `api.NewServer` and `deployer.New`
- `api.NewServer` now requires an `*events.Bus` parameter (6th arg before encKey)
- `deployer.New` now requires an `EventPublisher` parameter (6th arg before encKey)
- HTTP server `WriteTimeout` is 0 to support SSE
- The `web.go` file at project root uses package name `dockerwatcher` (imported as `github.com/alexei/docker-watcher`)
@@ -1,72 +0,0 @@
# Phase 12: Hardening
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Production hardening — blue-green deploys, promote flow, dashboard auth, graceful shutdown, structured logging, and config export.
## Tasks
- [ ] Task 1: Blue-green deploys — start new container, health check, swap NPM proxy, then stop old container (zero downtime)
- [ ] Task 2: Promote flow — enforce `promote_from` for production deploys (only tags running in source stage are eligible)
- [ ] Task 3: Local auth — username/password stored in SQLite (bcrypt hashed), login endpoint, session token (JWT or cookie)
- [ ] Task 4: OAuth2/OIDC auth — integration with Authentik or any OIDC provider (configurable client ID, client secret, discovery URL)
- [ ] Task 5: Auth settings UI — settings page to choose auth mode (local/OIDC), configure OIDC provider, manage local users
- [ ] Task 6: Auth middleware — protect all /api/* routes except webhook; check session/JWT/OIDC token
- [ ] Task 7: Graceful shutdown — handle SIGTERM/SIGINT, drain in-progress deploys, close DB, stop poller
- [ ] Task 8: Structured logging — JSON logs with deploy context (project, stage, tag, instance ID)
- [ ] Task 9: Config export — download current SQLite state as YAML (reverse of seed import)
- [ ] Task 10: Dockerfile — multi-stage build (build frontend + Go, copy to minimal image)
- [ ] Task 11: docker-compose.yml — production-ready compose file with volumes, network, env
- [ ] Task 12: Final wiring review — ensure all services are properly initialized and shut down
## Files to Modify/Create
- `internal/deployer/bluegreen.go` — blue-green deploy strategy
- `internal/deployer/promote.go` — promote flow logic
- `internal/auth/local.go` — local auth (bcrypt password hashing, session tokens)
- `internal/auth/oidc.go` — OAuth2/OIDC provider integration
- `internal/auth/middleware.go` — auth middleware (session/JWT/OIDC token validation)
- `internal/auth/models.go` — user model, auth settings, session store
- `internal/api/auth.go` — auth API endpoints (login, logout, OIDC callback, user management)
- `internal/config/export.go` — config export to YAML
- `internal/logging/logger.go` — structured JSON logger
- `internal/store/users.go` — user CRUD, auth settings persistence
- `web/src/routes/login/+page.svelte` — login page
- `web/src/routes/settings/auth/+page.svelte` — auth settings UI
- `cmd/server/main.go` — graceful shutdown, structured logging, auth init
- `Dockerfile` — multi-stage build
- `docker-compose.yml` — production compose file
## Acceptance Criteria
- Blue-green: zero downtime during deploy (old container serves until new one is healthy)
- Promote: production deploy only accepts tags from the specified source stage
- Auth: unauthenticated requests to /api/* (except webhook) return 401
- Graceful shutdown: in-progress deploys complete before exit
- Logs are JSON-formatted with contextual fields
- Config export produces valid YAML that could be re-imported
- Docker image builds and runs correctly
## Notes
- Blue-green: keep old container running until new one passes health check, then swap NPM proxy and stop old
- Auth has two modes configurable via settings:
- **Local auth**: username/password in SQLite (bcrypt hashed), JWT session tokens
- **OAuth2/OIDC**: integration with Authentik or any OIDC provider (client ID, secret, discovery URL)
- First launch: create default admin user with configurable password via ADMIN_PASSWORD env var
- OIDC flow: redirect to provider → callback → create/link local user → issue session
- SIGTERM handling: use Go's `os/signal` + `context.WithCancel`
- Structured logging: use `log/slog` (Go stdlib since 1.21)
- Dockerfile: build stage with Node.js + Go, runtime stage with scratch/alpine
- Phase 13 (UI Polish) and Phase 14 (Volumes & Env) follow this phase
## Review Checklist
- [ ] All tasks completed
- [ ] Blue-green deploy handles rollback if new container fails
- [ ] Auth doesn't block webhook endpoint
- [ ] Graceful shutdown tested with concurrent deploys
- [ ] Dockerfile produces a minimal image
- [ ] docker-compose.yml matches the example in PLAN.md
## Handoff to Next Phase
<!-- This is the final phase — no handoff needed. -->
@@ -1,86 +0,0 @@
# Phase 13: Frontend Polish & Modern UI
**Status:** COMPLETED
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Enhance the web UI with a modern, polished look and feel — custom SVG icons, refined typography, consistent color palette, smooth transitions, and overall professional frontend quality.
## Tasks
- [x] Task 1: Design system foundations — CSS custom properties for color palette (light/dark), spacing scale, typography scale, border radius tokens, shadows, transitions in `web/src/lib/styles/tokens.css`
- [x] Task 2: SVG icon set — 38 Lucide-based inline SVG icon components in `web/src/lib/components/icons/` covering all UI actions (deploy, stop, start, restart, remove, settings, registry, etc.)
- [x] Task 3: Refine layout — polished sidebar with active state indicators (dot + background), smooth transitions, responsive breakpoints, collapsible sidebar on mobile with hamburger menu
- [x] Task 4: Dashboard cards — redesigned project cards with box icon, status indicators, instance count badges, hover effects (-translate-y-0.5, shadow-md), port/healthcheck chips
- [x] Task 5: Project detail view — clean card layout for instances with icon action buttons, inline status badges with pulse animation for "running", deploy history as timeline cards
- [x] Task 6: Form styling — consistent input fields with design tokens, select dropdowns, ToggleSwitch component replacing checkboxes, button hierarchy (primary brand/secondary/danger)
- [x] Task 7: Toast/notification system — slide-in toasts with Lucide icons, rounded-xl, auto-dismiss, stacking
- [x] Task 8: Loading states — Skeleton, SkeletonCard, SkeletonTable loader components with shimmer animation for data fetching, IconLoader spinner for actions
- [x] Task 9: Empty states — EmptyState component with SVG illustrations and call-to-action buttons for all empty list scenarios
- [x] Task 10: Responsive design — mobile-friendly layout with collapsible sidebar, hamburger menu, mobile top bar, touch-friendly controls, horizontal settings nav on mobile
- [x] Task 11: Micro-interactions — button press feedback (active:animate-press), status pulse animation (ping), scale-in for dialogs/forms, fade-in for overlays, slide-in for toasts
- [x] Task 12: Dark mode support — ThemeToggle component with light/dark/system modes, CSS custom properties for dark theme via [data-theme="dark"], localStorage persistence, system preference detection
- [x] Task 13: Localization (EN/RU) — i18n store with derived t() function, en.json and ru.json locale files, LocaleSwitcher component, localStorage persistence, all UI strings translated
## Files Created
- `web/src/lib/styles/tokens.css` — design tokens (colors, spacing, typography, radius, shadows, transitions, animations)
- `web/src/lib/components/icons/` — 38 Lucide icon components + index.ts barrel export
- `web/src/lib/i18n/en.json` — English locale strings
- `web/src/lib/i18n/ru.json` — Russian locale strings
- `web/src/lib/i18n/index.ts` — i18n store with t() function and locale management
- `web/src/lib/stores/theme.ts` — dark mode store with system preference detection
- `web/src/lib/components/Skeleton.svelte` — base skeleton loader
- `web/src/lib/components/SkeletonCard.svelte` — card skeleton placeholder
- `web/src/lib/components/SkeletonTable.svelte` — table skeleton placeholder
- `web/src/lib/components/EmptyState.svelte` — empty state with SVG illustrations
- `web/src/lib/components/ToggleSwitch.svelte` — toggle switch replacing checkboxes
- `web/src/lib/components/ThemeToggle.svelte` — light/dark/system theme toggle
- `web/src/lib/components/LocaleSwitcher.svelte` — EN/RU locale switcher
## Files Modified
- `web/src/app.css` — imports tokens.css, adds base styles, custom scrollbar, focus ring utility
- `web/src/routes/+layout.svelte` — polished sidebar with icons, collapsible mobile sidebar, theme/locale controls
- `web/src/routes/+page.svelte` — dashboard with stats cards, skeleton loaders, empty states, i18n
- `web/src/routes/login/+page.svelte` — polished login with design tokens and i18n
- `web/src/routes/deploy/+page.svelte` — quick deploy with icons, animations, i18n
- `web/src/routes/projects/+page.svelte` — projects list with skeleton loaders, empty states, i18n
- `web/src/routes/projects/[id]/+page.svelte` — project detail with deploy timeline, icons, i18n
- `web/src/routes/projects/[id]/env/+page.svelte` — env editor with toggle switches, icons, i18n
- `web/src/routes/projects/[id]/volumes/+page.svelte` — volume editor with icons, i18n
- `web/src/routes/settings/+layout.svelte` — settings nav with icons, responsive horizontal nav
- `web/src/routes/settings/+page.svelte` — general settings with design tokens, i18n
- `web/src/routes/settings/registries/+page.svelte` — registries with icons, empty states, i18n
- `web/src/routes/settings/credentials/+page.svelte` — credentials with design tokens, i18n
- `web/src/routes/settings/auth/+page.svelte` — auth settings with icons, empty states, i18n
- `web/src/lib/components/Toast.svelte` — slide-in toasts with Lucide icons
- `web/src/lib/components/StatusBadge.svelte` — pulse animation for running status
- `web/src/lib/components/ConfirmDialog.svelte` — fade/scale-in animation, icon
- `web/src/lib/components/FormField.svelte` — consistent styling with design tokens
- `web/src/lib/components/ProjectCard.svelte` — redesigned with hover effects, badges
- `web/src/lib/components/InstanceCard.svelte` — icon action buttons, improved layout
## Acceptance Criteria
- [x] UI looks modern and professional — not "default framework" appearance
- [x] Consistent icon language throughout the app
- [x] Smooth transitions and meaningful animations (not gratuitous)
- [x] Responsive down to mobile viewport
- [x] Loading and empty states provide good UX
- [x] Color palette works well in both light and dark contexts
- [x] All UI strings available in English and Russian, switchable via locale picker
## Review Checklist
- [x] All tasks completed
- [x] Visual consistency across all pages
- [x] No functionality regressions
- [x] Responsive on mobile/tablet/desktop
- [x] Accessible (proper contrast ratios, focus states, aria labels on icons)
## Handoff Notes
This is the FINAL phase. All 13 phases of Docker Watcher are now complete. The application has:
- Full Go backend with SQLite, Docker management, Nginx Proxy Manager integration
- SvelteKit frontend with dark mode, i18n (EN/RU), responsive design, skeleton loaders, empty states
- Real-time SSE events for deploy/instance status
- Authentication (local + OIDC), RBAC, registry management
- Environment variable overrides, volume management, config export
- Webhook-based and polling-based image detection
@@ -1,58 +0,0 @@
# 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
<!-- Filled in by the implementation agent after completing this phase. -->
@@ -1,61 +0,0 @@
# Phase 2: Crypto & Config Seed Loader
**Status:** ✅ Complete
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Implement AES-256 encryption for credential storage and the YAML seed config parser that imports into SQLite on first launch.
## Tasks
- [x] Task 1: Implement AES-256-GCM encrypt/decrypt functions using Go stdlib `crypto/aes` + `crypto/cipher`
- [x] Task 2: Key derivation from ENCRYPTION_KEY env var (SHA-256 hash to get 32 bytes)
- [x] Task 3: Define YAML config structs matching the seed format from PLAN.md
- [x] Task 4: Implement YAML parser — read and validate seed file
- [x] Task 5: Implement seed importer — checks if DB is empty, if so imports YAML into SQLite via store CRUD
- [x] Task 6: Encrypt credential fields (registry tokens, NPM password) during import
- [x] Task 7: Create `docker-watcher.example.yaml` with documented example config
- [x] Task 8: Wire seed import into `cmd/server/main.go` startup sequence
## Files to Modify/Create
- `internal/crypto/crypto.go` — AES-256-GCM encrypt/decrypt
- `internal/config/config.go` — YAML structs and parser
- `internal/config/seed.go` — seed import logic (YAML → SQLite)
- `docker-watcher.example.yaml` — example seed config
- `cmd/server/main.go` — add seed import to startup
## Acceptance Criteria
- Encrypt then decrypt round-trips correctly
- Different plaintexts produce different ciphertexts (random nonce)
- YAML parsing handles all fields from the seed format
- Seed import creates projects, stages, registries, and settings in SQLite
- Credentials are encrypted before storage
- Import is idempotent — skipped if DB already has data
## Notes
- ENCRYPTION_KEY is the only secret env var — everything else is encrypted in SQLite
- Use GCM mode for authenticated encryption (integrity + confidentiality)
- Seed import should be transactional — all or nothing
- The example YAML should have placeholder values, not real credentials
## Review Checklist
- [x] All tasks completed
- [x] Crypto uses secure practices (random nonce, GCM, no ECB)
- [x] No hardcoded keys or secrets
- [x] YAML parsing validates required fields
- [x] Import is transactional
## Handoff to Next Phase
- `crypto.Encrypt(key, plaintext)` and `crypto.Decrypt(key, ciphertextHex)` handle AES-256-GCM encryption; ciphertext is hex-encoded with prepended nonce
- `crypto.KeyFromEnv()` derives a `[32]byte` key from the `ENCRYPTION_KEY` env var via SHA-256
- `crypto.EncryptIfNotEmpty(key, value)` is a convenience wrapper that passes through empty strings unchanged
- `config.ImportSeed(db, seedPath)` is the single entry point for seed import — called from `main.go` at startup
- Import is idempotent: skipped if the DB already has projects or registries
- Import is transactional: all inserts happen within a single SQLite transaction (rollback on any failure)
- Registry `token` and settings `npm_password` are now stored encrypted in SQLite — later phases that read these fields must decrypt with `crypto.Decrypt(key, value)`
- `store.DB()` method was added to expose the underlying `*sql.DB` for transaction use
- Seed file path is configurable via `SEED_FILE` env var (default: `./docker-watcher.yaml`)
- YAML validation ensures: `global.domain` is required, every project needs `image`, project registry references must exist, stages need `tag_pattern`
- `go.sum` still does not exist — run `go mod tidy` when Go toolchain is available
@@ -1,98 +0,0 @@
# Phase 3: Docker Client
**Status:** :white_check_mark: Complete
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Implement the Docker Engine API wrapper for container lifecycle management — pull images, inspect, create/start/stop/remove containers, and manage networks.
## Tasks
- [x] Task 1: Create Docker client wrapper with socket connection (`/var/run/docker.sock`)
- [x] Task 2: Implement `PullImage(ctx, image, tag, authConfig)` — pull with optional registry auth
- [x] Task 3: Implement `InspectImage(ctx, image)` — extract EXPOSE ports, HEALTHCHECK, labels
- [x] Task 4: Implement `CreateContainer(ctx, config)` — create with name, image, env, ports, network, labels
- [x] Task 5: Implement `StartContainer(ctx, containerID)`, `StopContainer(ctx, containerID, timeout)`, `RemoveContainer(ctx, containerID, force)`
- [x] Task 6: Implement `RestartContainer(ctx, containerID, timeout)`
- [x] Task 7: Implement `ListContainers(ctx, filters)` — filter by labels to find managed containers
- [x] Task 8: Implement `EnsureNetwork(ctx, networkName)` — create network if not exists
- [x] Task 9: Implement `ConnectNetwork(ctx, networkID, containerID)` — attach container to network
- [x] Task 10: Add docker-watcher labels to all managed containers (`docker-watcher.project`, `docker-watcher.stage`, `docker-watcher.instance-id`)
## Files to Modify/Create
- `internal/docker/client.go` — Docker client wrapper, connection setup
- `internal/docker/container.go` — container lifecycle operations
- `internal/docker/image.go` — pull and inspect operations
- `internal/docker/network.go` — network management
## Acceptance Criteria
- Client connects to Docker socket
- Pull handles both public and authenticated registries
- Image inspection extracts port, healthcheck, and label metadata
- Container creation applies all config (env, ports, network, labels)
- All operations return meaningful errors
- Managed containers are identifiable via labels
## Notes
- Use `github.com/docker/docker/client` SDK
- Container names should be deterministic: `dw-{project}-{stage}-{tag-sanitized}`
- All containers should be on the shared network (e.g., `staging-net`)
- Port mapping: container's EXPOSE port → random host port (Docker auto-assigns)
- Auth config for private registries will come from the store (encrypted tokens)
## Review Checklist
- [x] All tasks completed
- [x] Proper context propagation for cancellation
- [x] Resource cleanup (close client, remove failed containers)
- [x] No hardcoded values
- [x] Error messages include container/image identifiers
## Handoff to Next Phase
### Exported API surface (`internal/docker` package)
**Client lifecycle:**
- `docker.New() (*Client, error)` — creates client with env-based config and API version negotiation
- `(*Client).Close() error` — releases resources
- `(*Client).Ping(ctx) error` — checks daemon connectivity
**Image operations (`image.go`):**
- `(*Client).PullImage(ctx, imageRef, tag, authConfig) error` — pulls image; authConfig is base64-encoded JSON (use `EncodeRegistryAuth` helper)
- `(*Client).InspectImage(ctx, imageRef) (ImageInfo, error)` — returns `ImageInfo{ExposedPorts, Healthcheck, Labels}`
- `docker.EncodeRegistryAuth(username, password, serverAddress) (string, error)` — builds auth payload for `PullImage`
**Container operations (`container.go`):**
- `(*Client).CreateContainer(ctx, ContainerConfig) (containerID string, error)` — creates container with labels, env, ports, network
- `(*Client).StartContainer(ctx, containerID) error`
- `(*Client).StopContainer(ctx, containerID, timeoutSeconds) error`
- `(*Client).RemoveContainer(ctx, containerID, force) error`
- `(*Client).RestartContainer(ctx, containerID, timeoutSeconds) error`
- `(*Client).ListContainers(ctx, labelFilters) ([]ManagedContainer, error)` — always scoped to docker-watcher labels
- `(*Client).InspectContainerPort(ctx, containerID, containerPort) (uint16, error)` — gets auto-assigned host port
- `docker.ContainerName(project, stage, tag) string` — deterministic name: `dw-{project}-{stage}-{tag-sanitized}`
**Network operations (`network.go`):**
- `(*Client).EnsureNetwork(ctx, networkName) (networkID string, error)` — idempotent create-if-not-exists
- `(*Client).ConnectNetwork(ctx, networkID, containerID) error`
**Label constants:**
- `docker.LabelProject` = `"docker-watcher.project"`
- `docker.LabelStage` = `"docker-watcher.stage"`
- `docker.LabelInstanceID` = `"docker-watcher.instance-id"`
**Key types:**
- `docker.ContainerConfig` — input for `CreateContainer` (Name, Image, Env, ExposedPorts, NetworkName, NetworkID, Labels, Project, Stage, InstanceID)
- `docker.ImageInfo` — output of `InspectImage` (ExposedPorts, Healthcheck, Labels)
- `docker.ManagedContainer` — output of `ListContainers` (ID, Name, Image, Status, State, Project, Stage, InstanceID, Ports)
### Dependencies added
- `github.com/docker/docker v27.5.1+incompatible`
- `github.com/docker/go-connections v0.5.0`
- Run `go mod tidy` to resolve transitive dependencies before building
### Conventions maintained
- `context.Context` as first parameter on all methods
- Errors wrapped with `fmt.Errorf("context: %w", err)`
- Package-level constants for labels
- Immutable patterns (new maps created rather than mutating input)
@@ -1,78 +0,0 @@
# Phase 4: NPM Client
**Status:** ✅ Complete
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Implement the Nginx Proxy Manager API client — JWT authentication, CRUD for proxy hosts, and host lookup.
## Tasks
- [x] Task 1: Create NPM client struct with base URL, cached JWT token, and auto-refresh
- [x] Task 2: Implement `Authenticate(ctx, email, password)` — POST /api/tokens, store JWT
- [x] Task 3: Implement `CreateProxyHost(ctx, config)` — POST /api/nginx/proxy-hosts
- [x] Task 4: Implement `UpdateProxyHost(ctx, id, config)` — PUT /api/nginx/proxy-hosts/{id}
- [x] Task 5: Implement `DeleteProxyHost(ctx, id)` — DELETE /api/nginx/proxy-hosts/{id}
- [x] Task 6: Implement `ListProxyHosts(ctx)` — GET /api/nginx/proxy-hosts
- [x] Task 7: Implement `FindProxyHostByDomain(ctx, domain)` — search existing hosts by domain name
- [x] Task 8: Define proxy host config struct (domain, forward host/port, SSL settings, etc.)
- [x] Task 9: Handle JWT token expiry — re-authenticate automatically on 401
## Files to Modify/Create
- `internal/npm/client.go` — NPM API client, auth, HTTP helpers
- `internal/npm/types.go` — request/response types for proxy hosts
## Acceptance Criteria
- Client authenticates and caches JWT
- CRUD operations work for proxy hosts
- Token refresh happens transparently on expiry
- Proxy host config supports: domain, forward host, forward port, SSL (Let's Encrypt optional)
- FindByDomain enables checking if a proxy already exists before creating
## Notes
- NPM API base: typically `http://npm:81/api`
- Forward host for containers: use container name on the shared Docker network
- Forward port: the container's internal port (from EXPOSE)
- SSL: for staging, can be disabled; production may want Let's Encrypt
- NPM credentials come from settings (encrypted in SQLite, decrypted at runtime)
## Review Checklist
- [ ] All tasks completed
- [ ] JWT caching and refresh work correctly
- [ ] HTTP errors are properly handled (not just status code, but response body)
- [ ] No credentials logged or leaked in errors
- [ ] Struct types match NPM API contract
## Handoff to Next Phase
### What was built
- `internal/npm/types.go``ProxyHostConfig` (create/update input), `ProxyHost` (API response), `Meta`, auth types, and `boolInt` custom JSON type for NPM's 0/1 boolean fields.
- `internal/npm/client.go` — Full NPM API client with JWT auth, auto-refresh, and CRUD.
### Public API surface
```go
npm.New(baseURL string) *Client
(*Client).Authenticate(ctx, email, password string) error
(*Client).CreateProxyHost(ctx, config ProxyHostConfig) (ProxyHost, error)
(*Client).UpdateProxyHost(ctx, id int, config ProxyHostConfig) (ProxyHost, error)
(*Client).DeleteProxyHost(ctx, id int) error
(*Client).ListProxyHosts(ctx) ([]ProxyHost, error)
(*Client).FindProxyHostByDomain(ctx, domain string) (ProxyHost, bool, error)
```
### Key design decisions
- JWT token is cached with expiry; auto-refreshed 5 minutes before expiry or on 401.
- Credentials are stored in memory after `Authenticate` to enable transparent re-auth.
- All HTTP errors include the response body text for debugging.
- Credentials are never included in error messages.
- `boolInt` type handles NPM API's inconsistent 0/1 vs true/false for boolean fields.
- `FindProxyHostByDomain` does case-insensitive matching against all domain names.
### Dependencies for next phase
- Caller must provide decrypted NPM credentials (email + password from settings via `crypto.Decrypt`).
- `ProxyHost.ID` (int) maps to `Instance.NpmProxyID` in the store for tracking.
@@ -1,49 +0,0 @@
# Phase 5: Registry Client & Poller
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Implement the registry client interface with Gitea implementation, and the periodic tag polling scheduler.
## Tasks
- [ ] Task 1: Define `Registry` interface — `ListTags(ctx, image)`, `GetLatestTag(ctx, image, pattern)`
- [ ] Task 2: Implement Gitea registry client — uses Gitea API to list container image tags
- [ ] Task 3: Implement tag pattern matching — match tags against glob patterns (e.g., `dev-*`, `v*`)
- [ ] Task 4: Implement tag comparison — detect new tags since last poll (store last-seen tag per project/stage)
- [ ] Task 5: Create poller service — periodic scheduler using `robfig/cron`
- [ ] Task 6: Poller logic — for each project/stage with polling enabled, check for new tags, trigger deploy if auto_deploy
- [ ] Task 7: Add `last_polled_tag` field to instances or a new `poll_state` table in store
- [ ] Task 8: Implement registry factory — create client based on registry type (gitea, future: github, dockerhub)
## Files to Modify/Create
- `internal/registry/registry.go` — interface definition + factory
- `internal/registry/gitea.go` — Gitea registry implementation
- `internal/registry/poller.go` — polling scheduler service
- `internal/store/poll_state.go` — poll state persistence (optional, or extend existing tables)
## Acceptance Criteria
- Gitea client can list tags for a given image
- Tag pattern matching correctly filters tags (glob-style)
- Poller runs on configurable interval
- New tags are detected by comparing against stored state
- Registry factory returns correct client based on type
## Notes
- Gitea API: `GET /api/v1/packages/{owner}/container/{image}/tags` (or similar, verify against Gitea docs)
- Auth: Bearer token from registry config
- Polling interval comes from global settings
- The poller is a fallback — webhooks are the primary detection mechanism (Phase 6)
- GitHub Container Registry support is future work — just define the interface now
## Review Checklist
- [ ] All tasks completed
- [ ] Interface is clean and minimal
- [ ] Pattern matching handles edge cases (empty pattern, no tags)
- [ ] Poller doesn't leak goroutines
- [ ] Registry auth tokens handled securely
## Handoff to Next Phase
<!-- Filled in by the implementation agent after completing this phase. -->
@@ -1,78 +0,0 @@
# Phase 6: Webhook Handler
**Status:** ✅ Complete
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Implement the secret UUID-based webhook endpoint that receives image push notifications from CI systems, with auto-creation of unknown projects.
## Tasks
- [x] Task 1: Implement webhook HTTP handler — `POST /api/webhook/:secret-uuid`
- [x] Task 2: Validate incoming payload — extract image name and tag
- [x] Task 3: Look up project by image name in store — match against configured project images
- [x] Task 4: If known project: match tag to stage via tag patterns, determine if auto_deploy
- [x] Task 5: If unknown project: auto-create project with defaults from image inspection (EXPOSE port, labels)
- [x] Task 6: Generate and store webhook secret UUID in settings (on first launch)
- [x] Task 7: Implement webhook URL regeneration (new UUID, invalidates old one)
- [x] Task 8: Define webhook payload struct (`{"image": "registry/org/app:tag"}`)
## Files to Modify/Create
- `internal/webhook/handler.go` — webhook HTTP handler + payload parsing
- `internal/webhook/matcher.go` — project/stage matching logic
- `internal/webhook/autocreate.go` — auto-create project from unknown image
## Acceptance Criteria
- Valid webhook URL with correct UUID triggers processing
- Invalid/missing UUID returns 404 (no information leak)
- Known images are matched to projects and stages
- Unknown images trigger auto-creation with sensible defaults
- Webhook URL can be regenerated
## Notes
- Webhook URL format: `POST /api/webhook/d8f2a1e9-...`
- No authentication needed beyond the secret UUID
- Auto-created projects use: image EXPOSE port, "dev" as default stage, auto_deploy: true
- The webhook handler calls into the deployer (Phase 7) — for now, define the interface/callback
- Keep the handler thin — it matches and delegates
## Review Checklist
- [x] All tasks completed
- [x] No information leak on invalid UUIDs
- [x] Payload validation rejects malformed input
- [x] Auto-creation uses safe defaults
- [x] Handler is stateless (delegates to store/deployer)
## Handoff to Next Phase
### Exported API
- `webhook.NewHandler(store, deployer, inspector)` — creates the HTTP handler
- `webhook.Handler.Route()` — returns a `chi.Router` to mount at `/api/webhook`
- `webhook.EnsureWebhookSecret(store)` — generates UUID on first launch, returns current secret
- `webhook.RegenerateWebhookSecret(store)` — replaces secret with new UUID, invalidates old one
- `webhook.ParseImageRef(ref)` — parses `registry/owner/name:tag` into components
### Interfaces Defined
- `webhook.DeployTriggerer``TriggerDeploy(ctx, projectID, stageID, imageTag) error` (mirrors `registry.DeployTriggerer`)
- `webhook.ImageInspector``InspectImage(ctx, imageRef) (docker.ImageInfo, error)` (wraps `docker.Client`)
### Integration Points
- Mount the webhook router: `r.Mount("/api/webhook", webhookHandler.Route())`
- Call `webhook.EnsureWebhookSecret(store)` at application startup to generate the secret on first launch
- The deployer must implement `webhook.DeployTriggerer` (same signature as `registry.DeployTriggerer`)
- The Docker client (`*docker.Client`) satisfies `webhook.ImageInspector` directly
### Auto-Create Behavior
- Unknown images create a project with name from image name, port from EXPOSE, healthcheck from image metadata
- A default "dev" stage is created with `tag_pattern: "*"`, `auto_deploy: true`, `max_instances: 1`
- If image inspection fails (not pulled locally), project is created with port=0 and empty healthcheck
### Tag Matching
- Uses `path.Match` (glob semantics) — same approach as the registry poller
- Stages are checked in name-sorted order; first matching stage wins
@@ -1,54 +0,0 @@
# Phase 7: Deployer & Health Checker
**Status:** ✅ Complete
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Implement the core deployment orchestrator: pull → start container → configure NPM proxy → health check → success/rollback. Plus multi-instance support and notifications.
## Tasks
- [x] Task 1: Define deployer service struct — depends on Docker client, NPM client, store, notifier
- [x] Task 2: Implement deploy flow: pull image → create container → start → connect to network → configure proxy → health check
- [x] Task 3: Implement subdomain generation per convention: `stage-{stage}-{project}` for default, `stage-{stage}-{project}-{tag}` for specific
- [x] Task 4: Sanitize tags for DNS (dots → dashes, lowercase, truncate)
- [x] Task 5: Implement health checker — HTTP GET to `http://container:{port}{healthcheck_path}` with retries and timeout
- [x] Task 6: Implement rollback on health check failure — remove new container, delete NPM proxy host, update instance status
- [x] Task 7: Implement multi-instance support — multiple tags of same project/stage can run simultaneously
- [x] Task 8: Implement max_instances enforcement — remove oldest instance when limit reached
- [x] Task 9: Implement notification webhook — POST to configured URL on deploy success/failure
- [x] Task 10: Create deploy history records in store (status, timestamps, logs)
- [x] Task 11: Implement deploy log streaming — append log entries during deploy for real-time visibility
## Files to Modify/Create
- `internal/deployer/deployer.go` — main deploy orchestrator
- `internal/deployer/subdomain.go` — subdomain generation and DNS sanitization
- `internal/deployer/rollback.go` — rollback logic
- `internal/health/checker.go` — HTTP health checker with retries
- `internal/notify/notifier.go` — webhook notification sender
## Acceptance Criteria
- Full deploy flow works end-to-end (pull → proxy → health check)
- Failed health checks trigger automatic rollback
- Multi-instance: deploying a new tag doesn't stop existing instances
- max_instances removes oldest instance when exceeded
- Notifications fire on success and failure
- Deploy history is recorded with status and timestamps
## Notes
- Health check: 3 retries, 5s between retries, 10s timeout per attempt (configurable later)
- Subdomain pattern comes from global settings
- Notifications are fire-and-forget (don't block deploy on notification failure)
- Deploy logs should be structured entries (timestamp + message) for SSE streaming later
- The deployer is the central orchestrator — webhook handler and poller both call into it
## Review Checklist
- [ ] All tasks completed
- [ ] Rollback cleans up ALL resources (container, proxy, instance record)
- [ ] No goroutine leaks
- [ ] Error handling at every step of the deploy flow
- [ ] Subdomain generation produces valid DNS names
## Handoff to Next Phase
<!-- Filled in by the implementation agent after completing this phase. -->
@@ -1,112 +0,0 @@
# Phase 8: REST API Layer
**Status:** ✅ Complete
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Wire up all REST API endpoints using chi router, connecting the store, deployer, and other services to HTTP handlers.
## Tasks
- [x] Task 1: Set up chi router with middleware (logging, recovery, CORS, JSON content-type)
- [x] Task 2: Implement project endpoints — GET/POST /api/projects, GET/PUT/DELETE /api/projects/:id
- [x] Task 3: Implement stage endpoints — POST /api/projects/:id/stages, PUT/DELETE /api/projects/:id/stages/:stage
- [x] Task 4: Implement instance endpoints — GET /api/projects/:id/stages/:stage/instances, POST (deploy), DELETE (remove)
- [x] Task 5: Implement instance control endpoints — POST .../instances/:iid/stop, start, restart
- [x] Task 6: Implement quick deploy endpoints — POST /api/deploy/inspect, POST /api/deploy/quick
- [x] Task 7: Implement registry endpoints — GET/POST /api/registries, PUT/DELETE /api/registries/:id, POST .../test
- [x] Task 8: Implement settings endpoints — GET/PUT /api/settings, GET /api/settings/webhook-url, POST .../regenerate
- [x] Task 9: Implement deploy history endpoints — GET /api/deploys, GET /api/deploys/:id/logs (SSE stub)
- [x] Task 10: Implement registry tags endpoint — GET /api/registries/:id/tags/:image
- [x] Task 11: Wire webhook handler into router — POST /api/webhook/:secret-uuid
- [x] Task 12: Wire everything in main.go — initialize all services, start HTTP server
## Files to Modify/Create
- `internal/api/router.go` — chi router setup, middleware
- `internal/api/projects.go` — project CRUD handlers
- `internal/api/stages.go` — stage CRUD handlers
- `internal/api/instances.go` — instance lifecycle handlers
- `internal/api/deploys.go` — deploy + quick deploy handlers
- `internal/api/registries.go` — registry CRUD + test + tags handlers
- `internal/api/settings.go` — settings handlers
- `internal/api/middleware.go` — middleware (logging, CORS, recovery)
- `internal/api/response.go` — consistent API response helpers (envelope format)
- `cmd/server/main.go` — full service wiring and HTTP server start
## Acceptance Criteria
- All endpoints from the API spec in PLAN.md are implemented
- Consistent JSON envelope response format (success, data, error, metadata)
- CORS configured for frontend dev (localhost origins)
- Proper HTTP status codes (200, 201, 400, 404, 500)
- main.go starts a fully wired HTTP server
## Notes
- Response envelope: `{"success": bool, "data": any, "error": string|null, "meta": {pagination}}`
- CORS: allow all origins in dev, restrict in production (configurable later)
- SSE for deploy logs is a stub in this phase — real implementation in Phase 11
- Quick deploy: /inspect pulls and inspects image, returns defaults; /quick creates project + deploys
- All handlers should validate input and return 400 for bad requests
## Review Checklist
- [x] All tasks completed
- [x] All API endpoints from PLAN.md are covered
- [x] Consistent response format across all endpoints
- [x] Input validation on all POST/PUT handlers
- [x] No business logic in handlers (delegates to services)
## Handoff to Next Phase
### API Surface
- `api.NewServer(store, docker, deployer, webhookHandler, encKey)` creates the server
- `server.Router()` returns a `chi.Router` with all routes mounted under `/api`
- Response envelope: `{"success": bool, "data": ..., "error": "..."}`
### Endpoints Implemented
| Method | Path | Handler |
|--------|------|---------|
| GET | /api/projects | listProjects |
| POST | /api/projects | createProject |
| GET | /api/projects/{id} | getProject (includes stages) |
| PUT | /api/projects/{id} | updateProject |
| DELETE | /api/projects/{id} | deleteProject |
| POST | /api/projects/{id}/stages | createStage |
| PUT | /api/projects/{id}/stages/{stage} | updateStage |
| DELETE | /api/projects/{id}/stages/{stage} | deleteStage |
| GET | /api/projects/{id}/stages/{stage}/instances | listInstances |
| POST | /api/projects/{id}/stages/{stage}/instances | deployInstance |
| DELETE | /api/projects/{id}/stages/{stage}/instances/{iid} | removeInstance |
| POST | .../instances/{iid}/stop | stopInstance |
| POST | .../instances/{iid}/start | startInstance |
| POST | .../instances/{iid}/restart | restartInstance |
| GET | /api/deploys | listDeploys |
| GET | /api/deploys/{id}/logs | getDeployLogs (JSON stub) |
| POST | /api/deploy/inspect | inspectImage |
| POST | /api/deploy/quick | quickDeploy |
| GET | /api/registries | listRegistries |
| POST | /api/registries | createRegistry |
| PUT | /api/registries/{id} | updateRegistry |
| DELETE | /api/registries/{id} | deleteRegistry |
| POST | /api/registries/{id}/test | testRegistry |
| GET | /api/registries/{id}/tags/* | listRegistryTags |
| GET | /api/settings | getSettings |
| PUT | /api/settings | updateSettings |
| GET | /api/settings/webhook-url | getWebhookURL |
| POST | /api/settings/regenerate | regenerateWebhookSecret |
| POST | /api/webhook/{secret} | webhook handler (mounted from webhook package) |
### main.go Wiring
- All services initialized: store, docker, npm, deployer, health, notifier, webhook, poller
- HTTP server with graceful shutdown on SIGTERM/SIGINT
- Environment variables: `DATA_DIR`, `SEED_FILE`, `ENCRYPTION_KEY`, `NPM_URL`, `POLLING_INTERVAL`, `LISTEN_ADDR`
- Default listen address: `:8080`
### SSE Stub
- `GET /api/deploys/{id}/logs` returns logs as JSON array (not SSE yet)
- Real SSE streaming deferred to Phase 11
### Security Notes
- Registry tokens are encrypted before storage, decrypted on read for API calls
- Settings response strips `npm_password` and `webhook_secret`, returns `has_npm_password` boolean
- Registry list response strips tokens, returns `has_token` boolean
- CORS allows all origins (dev mode) -- restrict in Phase 12
@@ -1,99 +0,0 @@
# Phase 9: SvelteKit Dashboard & Project Views
**Status:** ✅ Complete
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Build the SvelteKit frontend with the dashboard overview and project detail views — project list, instance status, controls, and deploy history.
## Tasks
- [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
- `web/svelte.config.js` — SvelteKit config with static adapter
- `web/vite.config.ts` — Vite config with API proxy for dev
- `web/src/app.html` — base HTML
- `web/src/lib/api.ts` — API client
- `web/src/lib/types.ts` — shared TypeScript types
- `web/src/routes/+layout.svelte` — app layout with navigation
- `web/src/routes/+page.svelte` — dashboard
- `web/src/routes/projects/+page.svelte` — project list
- `web/src/routes/projects/[id]/+page.svelte` — project detail
- `web/src/lib/components/StatusBadge.svelte` — status indicator
- `web/src/lib/components/InstanceCard.svelte` — instance display
- `web/src/lib/components/ProjectCard.svelte` — project summary card
- `web/src/lib/components/ConfirmDialog.svelte` — confirmation modal
## Acceptance Criteria
- SvelteKit project builds to static output
- Dashboard shows all projects with live status
- Project detail shows stages, instances, and controls
- Instance controls trigger correct API calls
- Deploy dropdown fetches and displays available tags
- UI is responsive and clean
## Notes
- SvelteKit static adapter for embedding in Go binary
- API proxy in vite.config.ts for dev: proxy `/api` to `http://localhost:8080`
- Use SvelteKit's `fetch` for SSR-compatible data loading
- Status colors: green=running, yellow=starting, red=failed, gray=stopped
- Keep components small and reusable
## Review Checklist
- [ ] All tasks completed
- [ ] TypeScript types match backend API response format
- [ ] API client handles errors gracefully with user feedback
- [ ] No hardcoded API URLs (use relative paths)
- [ ] 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.