Wraps up the workload refactor with the fixes that came out of the multi-agent
code review (see docs/plans/workload-refactor.md "What actually shipped").
Backend:
- store.ReconcileContainer: separate write path so the 30s reconciler tick no
longer overwrites deployer-owned fields (subdomain, proxy_route_id,
npm_proxy_id, image_tag).
- Container.stage_id column + index; ListProxyRoutes / ListContainersByStageID
join via stage_id (survives stage rename), with legacy fallback to
(project_id, role=stage_name).
- Reconciler: workload-existence check (rejects forged tinyforge.workload.id
labels), skips inventing project-kind rows, child-context cancel before
wg.Wait() on shutdown.
- Transactional CRUD across projects / stacks / static_sites: parent UPDATE
and workload sync land in one transaction so secret rotations are durable.
- Webhook routing reads exclusively through workloads.webhook_secret; legacy
GetProjectByWebhookSecret / GetStaticSiteByWebhookSecret fallback removed.
- store.GetStackByComposeProjectName + indexed lookup (no more full-table
stack scan per compose container per tick).
- store.ListMissingSweepRows: filtered query for the missing-sweep.
- /api/instances/* handlers verify (workload_id, role) match URL
(project_id, stage_name) before mutating — closes the cross-project
hijack the security review flagged.
- extra_json no longer referenced from Go (column kept on disk for now).
Frontend:
- WorkloadContainers.svelte: generic detail-page panel reusable by stack and
site detail pages.
- Containers page polish: client-side kind/state filters over an unfiltered
fetch, URL-synced filters, race-safe loads via sequence number, EN+RU i18n,
sidebar counter via navCounts.containers.
Misc:
- scripts/dev-server.sh: tolerate empty netstat grep result.
- .gitignore: ignore docker-watcher binaries, .claude/worktrees/, .facts-sync.json.
- Add concurrency limiter (max 4 GET requests) to API layer, leaving
slots for SSE and health checks. Write ops bypass the limiter.
- Add AbortController to ContainerStats, project detail page, and
dashboard to cancel in-flight requests on navigation/unmount.
- Move global SSE connection from layout to events page (only consumer).
- Add 30s heartbeat to SSE endpoint to detect zombie connections.
- Serialize dashboard project fetches to avoid parallel burst.
- Rebuild frontend in dev-server.sh so go:embed stays in sync.
Rebrand the project as Tinyforge to reflect its evolution from a Docker
container watcher into a self-hosted mini CI/deployment platform.
Rename covers: Go module path, Docker labels, DB/config filenames,
JWT issuer, Dockerfile binary, docker-compose, CI workflows, frontend
i18n, README with static sites docs, and all code comments.
- Add proxy provider selector (None/NPM) to settings page with radio cards
- Show warning when switching to None about existing NPM routes
- Conditionally hide SSL certificate picker and NPM credentials when provider is None
- Fix webhook URL regenerate not updating UI (field name mismatch: url vs webhook_url)
- Fix dev-server.sh to persist ENCRYPTION_KEY across restarts via data/.dev-key
- Fix selected radio card background visibility in dark mode
- CRITICAL: Change DNS zones endpoint from GET to POST to avoid
leaking API token in URL query parameters
- HIGH: Add sync.RWMutex to protect dnsProvider field in Server,
Deployer, and proxy Manager against concurrent read/write races
- HIGH: Capture old DNS provider reference synchronously before
launching background cleanup goroutine
- HIGH: Use getDNS()/getDNSProviderLocked() accessors instead of
direct field reads in all DNS operations