# Phase 2 — Store + API (manual sync, explicit-action) ## Tasks - [ ] **Store setters** (`internal/store/workloads.go` or a new `gitops.go`): - `SetWorkloadGitOps(id string, enabled bool, path string) error` — gated to dockerfile/static at the API layer. - `RecordGitOpsSync(id, commitSHA, syncedAt string) error`. - All writes re-read the row first / use targeted column updates (avoid full-row clobber races — review S5). - [ ] **Sync audit** (NOT deploy_history): a small `gitops_sync_audit` table (`id, workload_id, outcome, commit_sha, drift_count, error, created_at`) with an insert helper. Errors stored as generic markers only (secret-safe). _(Or reuse the event log if cleaner — pick one and note it.)_ - [ ] **API handlers** (`internal/api/gitops.go`, wired in `internal/api/router.go`): - `GET /api/workloads/{id}/gitops` → `{ enabled, path, status, raw, parsed, commit_sha, last_sync_at, drift_count }` (calls `gitops.Fetch` + `gitops.Drift`). - `GET /api/workloads/{id}/gitops/drift` → `[]DriftEntry`. - `POST /api/workloads/{id}/gitops/sync` (`auth.AdminOnly`) → `Fetch` → `MergeAndValidate` → `UpdateWorkload` (single txn) → `RecordGitOpsSync` + audit. Returns the applied summary. Secret-safe errors. - `PUT /api/workloads/{id}/gitops` (`auth.AdminOnly`) → enable/disable + path; **reject if source_kind ∉ {dockerfile, static}** with a clear 400. - [ ] **Validation**: path must be a repo-relative file (no `..`, no leading `/`, sane length); `enabled` only when the source is git-backed and has repo coords. ## Verify - `go build ./...`, `go vet ./internal/...`, `go test ./internal/...` green. - Handler tests: admin-gate on sync/put, no_file path, secret-safe error on a failed fetch, drift_count surfaced, non-git source rejected by PUT. ## Handoff notes _(filled after implementation)_