# 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)_
