# Workload-First Refactor — Remaining Work Handoff for resuming the refactor. The plugin architecture (Source × Trigger), `/api/workloads` surface, `/apps` UI, env/volume/webhook/logs/chain panels, multi-face proxy routes, blue-green image deploys, schema-driven wizard, and test coverage on triggers / image helpers / webhook parser / store upserts are **already landed and live**. What follows is what's still pending, in priority order. ## Status at a glance | Item | Priority | Status | | ---- | -------- | ------ | | Static source inline port | 1 | **PENDING** — only remaining blocker for hard cutover | | Hard legacy cutover | 1 | **PENDING** — gated by static port (volume scopes blocker is resolved) | | Generalized volume scopes | 2 | DONE | | Kind-aware editors (compose / image / static) | 2 | DONE | | Vendor-specific webhook parsing | 2 | DONE | | Chain-panel CSS | 3 | DONE | | Log Rules panel on `/apps/[id]` | adjacent | DONE — uses `getEffectiveLogScanRules` + per-workload override action | | i18n for `/apps/*` page strings | 3 | **PARTIAL** — Log Rules panel + Observability surfaces i18n'd; `apps.*` namespace still pending | | Docs / codemap entries for `internal/workload/plugin/` | 3 | **PENDING** | | API-handler / dispatcher / compose-source / static-backend tests | 4 | **PENDING** | | Triggers as first-class reusable entities (post-cutover) | 5 | **PENDING** | Cross-references to the adjacent Observability work (Event Triggers + Log Scanner backend + drop-counter stats panel) live in [docs/LOGSCAN_AND_TRIGGERS_TODO.md](LOGSCAN_AND_TRIGGERS_TODO.md). ## Priority 1 — Architecture unlock ### Static source inline port — ~2150 LOC across 8 files The current `internal/workload/plugin/source/static/` delegates to `staticsite.Manager` via a phantom-row adapter (`cmd/server/static_backend.go`) that keeps a synthetic row in the legacy `static_sites` table per workload. This works but blocks the hard cutover — you can't drop `static_sites` until the adapter is gone. To port inline, the deploy pipeline body has to move into `internal/workload/plugin/source/static/`: | Source file | Lines | What to keep / port | | --- | --- | --- | | `internal/staticsite/manager.go` | 834 | Deploy / Stop / status pipeline. State should move to `containers` rows + `workload_env` instead of `static_sites`. | | `internal/staticsite/gitea_content.go` | 360 | Keep as helper — Gitea content download/listing. | | `internal/staticsite/github_provider.go` | 276 | Keep as helper. | | `internal/staticsite/gitlab_provider.go` | 254 | Keep as helper. | | `internal/staticsite/healthcheck.go` | 111 | Convert to plugin Reconcile body. | | `internal/staticsite/markdown.go` | 83 | Keep as helper. | | `internal/staticsite/provider.go` | 171 | Keep — provider abstraction. | | `internal/staticsite/deno/` | (sub-pkg) | Keep — Dockerfile + router.ts codegen. | Estimated as its own dedicated turn (or two). Strategy: keep the provider abstraction + helpers exported; rewrite only `Manager.Deploy` body into a new `source/static/deploy.go` that operates against `plugin.Workload` directly and writes container rows + workload_env rather than the `static_sites` table. ### Hard legacy cutover Sole remaining blocker is the static source inline port above. The generalized-volume-scopes blocker is resolved (legacy `ResolvePath` stays in place for legacy callers and dies with the cutover). When the static port lands: - Delete `/api/projects`, `/api/stacks`, `/api/sites`, `/api/stages` handlers. - Drop tables: `projects`, `stages`, `stacks`, `stack_revisions`, `stack_deploys`, `static_sites`, `static_site_secrets`, `deploys`, `poll_states`. - Delete `internal/stack/`, `internal/staticsite/` packages. - Delete frontend `/projects`, `/sites`, `/stacks` routes. - Delete legacy `volume.ResolvePath` + `internal/api/volume_browser.go` callers (the only remaining users). ## Priority 2 — Behavior gaps ### ~~Generalized volume scopes~~ — DONE Landed: `internal/volume.ResolveWorkloadPath` (workload-keyed; sits next to the legacy `ResolvePath` so legacy code paths keep working) plus the wired-through `computeMounts` in `internal/workload/plugin/source/image/image.go`. All `VolumeScope` values are now honored at deploy time: - `absolute` — host bind, validated against `settings.AllowedVolumePaths`. - `ephemeral` — tmpfs. - `instance` — per-tag dir under `/-/instance-/`. - `stage`, `project` — both collapse to `/-/`. - `project_named` — Docker named volume prefixed `tf--`. - `named` — Docker named volume by raw name. Test coverage: `internal/volume/resolver_test.go` (table-driven, portable Linux/Windows). The legacy `ResolvePath` stays in place for legacy deployer + volume-browser callers and dies with the hard cutover. ### ~~Kind-aware editors on `/apps/new` and `/apps/[id]` edit~~ — DONE All three Source plugins now have hand-rolled forms on both pages, with an "Advanced JSON" toggle preserved as the power-user escape hatch. Submit logic marshals form fields back into the same JSON shape the backend already expects — no API or store changes required. **Principle:** the plugin contract makes new Source / Trigger kinds cheap on the backend, but the UI is not cheap by default — every kind needs a paired hand-rolled form to be daily-driver usable. The shared JSON editor is the fallback for power users and brand-new plugins, not the end state. New Source / Trigger merge requests should treat "ship the kind-aware form" as part of done, not a follow-up. **Landed:** - `compose`: YAML textarea + project_name input on both `/apps/new` and `/apps/[id]`. - `image`: form fields for image / port / healthcheck / default_tag / registry_name / cpu_limit / memory_limit / max_instances on both pages. Registry name is a select populated from `/api/registries` (with text-input fallback when the list is empty). env + volumes stay in their detail-page panels and round-trip through the form via `imageFormBody` so manual edits aren't clobbered. - `static`: provider select (gitea / github / gitlab), base URL, repo_owner / repo_name (both required), branch (default "main"), folder_path, access_token (password input, for private repos), mode radio (static / deno), render_markdown checkbox. The storage_enabled / storage_limit_mb fields aren't surfaced as form controls yet, but they round-trip through `staticFormBody` so values set via the raw JSON editor survive form edits. **Still pending forms:** none — all three Source plugins now have hand-rolled forms on both `/apps/new` and `/apps/[id]`. The raw JSON editor stays available behind the "Advanced JSON" toggle (shipped with compose) so the plugin's full sample is still reachable for power users and for any new plugin kind without a hand-rolled form. Effort: per-kind form roughly half a turn each; can land incrementally. Touches `web/src/routes/apps/new/+page.svelte` and the edit block in `web/src/routes/apps/[id]/+page.svelte`. The Svelte side keeps serializing into the same `source_config` JSON shape the backend already expects — no API or store change required. ### ~~Vendor-specific webhook parsing for `/api/webhook/workloads/{secret}`~~ — DONE Landed: `internal/webhook/vendor_parsers.go` plus rewrites in `internal/webhook/handler.go` `buildInboundEvent`. The dispatch order is now: 1. Empty body → manual event. 2. Vendor-specific parsers, short-circuit on a recognized `X-*-Event` header — Gitea package, GitHub `package` / `registry_package`, GitHub push, Gitea push, GitLab `Push Hook` / `Tag Push Hook`. 3. Generic simple-body fallback: top-level `image` or top-level `ref` — what the legacy CI integrations already send. Vendor parsers can populate fields the generic parser cannot: image digest, `GitEvent.Vendor`, registry host. When a vendor parser claims a request (header matches) it is authoritative — a malformed Gitea package payload surfaces as an error rather than silently falling through to the generic parser. Test coverage: `internal/webhook/vendor_parsers_test.go` covers each vendor branch + the routed-via-`buildInboundEvent` integration cases. Open follow-ups deferred to future turns: - GitLab Container Registry events use a custom envelope outside the webhook event surface — handle if a user reports needing it. - Docker Hub webhook (push event) uses `{"push_data": {"tag": ...}, "repository": {...}}` — add when there's a user request. ## Priority 3 — Polish ### ~~Chain-panel CSS~~ — DONE Landed: rules for `.chain-row`, `.chain-card` (with hover/transform on anchors), `.chain-self` (brand-tinted highlight), `.chain-name`, `.chain-label` (70px fixed-width mono column), `.chain-children-list` (flex-wrap), plus a sub-600px stack to keep the panel usable on narrow screens. Appended at the end of the `