diff --git a/plans/cloudflare-dns-management/CONTEXT.md b/plans/cloudflare-dns-management/CONTEXT.md deleted file mode 100644 index 1e5563b..0000000 --- a/plans/cloudflare-dns-management/CONTEXT.md +++ /dev/null @@ -1,33 +0,0 @@ -# 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 diff --git a/plans/cloudflare-dns-management/PLAN.md b/plans/cloudflare-dns-management/PLAN.md deleted file mode 100644 index 7717a46..0000000 --- a/plans/cloudflare-dns-management/PLAN.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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` diff --git a/plans/cloudflare-dns-management/phase-1-settings-model.md b/plans/cloudflare-dns-management/phase-1-settings-model.md deleted file mode 100644 index 8ee842c..0000000 --- a/plans/cloudflare-dns-management/phase-1-settings-model.md +++ /dev/null @@ -1,59 +0,0 @@ -# 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 - diff --git a/plans/cloudflare-dns-management/phase-2-cloudflare-client.md b/plans/cloudflare-dns-management/phase-2-cloudflare-client.md deleted file mode 100644 index e9f6ac5..0000000 --- a/plans/cloudflare-dns-management/phase-2-cloudflare-client.md +++ /dev/null @@ -1,61 +0,0 @@ -# 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 ` 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 - diff --git a/plans/cloudflare-dns-management/phase-3-dns-hooks.md b/plans/cloudflare-dns-management/phase-3-dns-hooks.md deleted file mode 100644 index b347d0a..0000000 --- a/plans/cloudflare-dns-management/phase-3-dns-hooks.md +++ /dev/null @@ -1,67 +0,0 @@ -# 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 - diff --git a/plans/cloudflare-dns-management/phase-4-settings-ui.md b/plans/cloudflare-dns-management/phase-4-settings-ui.md deleted file mode 100644 index 7254f41..0000000 --- a/plans/cloudflare-dns-management/phase-4-settings-ui.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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 - diff --git a/plans/cloudflare-dns-management/phase-5-dns-records-page.md b/plans/cloudflare-dns-management/phase-5-dns-records-page.md deleted file mode 100644 index acce225..0000000 --- a/plans/cloudflare-dns-management/phase-5-dns-records-page.md +++ /dev/null @@ -1,65 +0,0 @@ -# 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 - diff --git a/plans/cloudflare-dns-management/phase-6-dns-sync.md b/plans/cloudflare-dns-management/phase-6-dns-sync.md deleted file mode 100644 index 285f04a..0000000 --- a/plans/cloudflare-dns-management/phase-6-dns-sync.md +++ /dev/null @@ -1,62 +0,0 @@ -# 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 -