Files
Gitea-Tools/docs/webui-project-registry-api.md
T
sysadminandClaude Opus 4.8 b2f6e9a6dc feat(webui): versioned project registry API (Closes #635)
Evolve the MVP project registry (#427) into a versioned, fail-closed
project registry API for the console (Phase 1, read-only).

- Add schema version 2 with project `status`, per-step onboarding
  `state`/`required`, optional redacted `last_seen_health`, and
  `remote_name`. Version 1 files stay loadable and are normalized with
  explicit defaults.
- Serve `/api/v1/projects` and `/api/v1/projects/{project_id}` with API
  provenance (`api_version`, `schema_version`, `source`). `/api/projects`
  is retained as an unversioned Phase 1 alias.
- Replace bare `ValueError` with `RegistryError`, carrying an operator
  `remediation` and `field_path`; invalid registries fail closed as a
  500 JSON payload or a dedicated HTML error page instead of a traceback.
- Reject credential-shaped keys before any DTO is built, reusing
  `registry_safety.is_forbidden_key` as the single source of truth
  shared with the worker registry (#798).
- Render HTML views from `project_to_dict`, so the console and the JSON
  API cannot disagree about status or onboarding progress.
- Document the contract in docs/webui-project-registry-api.md.

Tests: registry load/validate (valid, missing project, schema
validation, credential rejection) and API route coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-22 16:12:07 -04:00

7.9 KiB

Project registry API (#635)

Phase 1 of the console architecture ADR gives the project registry a versioned, read-only API. This document is the field-by-field contract for that API and for the registry file behind it.

Everything here is read-only. The console never writes the registry; an operator edits the JSON file, and an invalid file fails closed rather than rendering a partial inventory.

Routes

Route Method Description
/api/v1/projects GET Versioned registry export: all projects, with provenance
/api/v1/projects/{project_id} GET Single project; 404 with project_not_found when unknown
/api/projects GET Unversioned MVP alias (#427), retained for all of Phase 1
/projects GET HTML list — status and onboarding progress per project
/projects/{project_id} GET HTML detail — identity, profiles, paths, checklist

Per ADR section 6 the unversioned alias may be retired no earlier than Phase 2, and only after this document and webui-local-dev.md record the swap. The alias returns the same payload as /api/v1/projects, including the legacy version and source_path keys #427 consumers already read.

The HTML views render from the same DTO the JSON routes serialize (project_to_dict), so the console and the API cannot disagree about a project's status or onboarding progress.

Registry file

Default location: webui/data/projects.registry.json. Override with the WEBUI_PROJECT_REGISTRY environment variable.

Schema versions: 1 and 2 are accepted; 2 is current. A version 1 file loads unchanged and is normalized with the documented defaults, so an existing operator registry keeps working without edits.

Root

Field Type Required Notes
version int yes 1 or 2. Anything else fails closed
projects array yes Must be non-empty

Project

Field Type Required Default Notes
id string yes Stable registry id used in URLs
repo_name string yes Gitea repository name
gitea_owner string yes Owning org or user
remote_host string yes Instance base URL, no credentials
remote_name string no null Logical remote label, e.g. prgs (v2)
default_branch string yes Stable branch name
local_checkout_path string yes Control checkout path
status string no active active, onboarding, paused, archived (v2)
profiles object yes Must map author, reviewer, reconciler
workflow_paths object yes Non-empty; label to repo-relative path
schema_paths object no {} Label to repo-relative path
onboarding_checklist array no [] See below
last_seen_health object no null Redacted health only (v2)

Onboarding step

Field Type Required Default Notes
id string yes Stable step id
title string yes Short operator-facing label
description string yes Self-contained; assumes no chat history
state string no pending complete, pending, blocked, not_applicable (v2)
required bool no true Optional steps never block readiness (v2)

Last-seen health

Field Type Required Notes
status string no (default unknown) healthy, degraded, unreachable, unknown
checked_at string no ISO-8601 UTC timestamp, e.g. 2026-01-01T00:00:00Z
detail string no Short redacted note

Health is recorded metadata, not a live probe: Phase 1 performs no outbound health checks. Endpoints, tokens, and keychain identifiers must never appear here.

Response shape

GET /api/v1/projects:

{
  "api_version": "v1",
  "schema_version": 2,
  "version": 2,
  "source_path": "/path/to/webui/data/projects.registry.json",
  "source": {
    "kind": "file",
    "path": "/path/to/webui/data/projects.registry.json",
    "inventory_complete": true
  },
  "project_count": 1,
  "projects": [
    {
      "id": "example",
      "repo_name": "Example",
      "gitea_owner": "Org",
      "repo_full_name": "Org/Example",
      "remote_host": "https://gitea.example.invalid",
      "remote_name": "example-remote",
      "default_branch": "main",
      "local_checkout_path": ".",
      "status": "active",
      "profiles": {"author": "...", "reviewer": "...", "reconciler": "..."},
      "workflow_paths": {"skill": "skills/..."},
      "schema_paths": {},
      "onboarding_checklist": [
        {
          "id": "profiles",
          "title": "Configure execution profiles",
          "description": "...",
          "state": "complete",
          "required": true
        }
      ],
      "onboarding_summary": {
        "total": 1,
        "complete": 1,
        "pending": 0,
        "blocked": 0,
        "not_applicable": 0,
        "required_outstanding": 0,
        "onboarding_complete": true
      },
      "last_seen_health": null
    }
  ]
}

GET /api/v1/projects/{project_id} returns api_version, schema_version, source, and a single project object with the same fields.

The source block satisfies the ADR section 6 provenance rule: every payload states where the data came from and whether the inventory is complete. A file-backed registry is always complete — there is no pagination to truncate it.

onboarding_summary is derived, never stored. required_outstanding counts steps that are required and in state pending or blocked; onboarding_complete is true when that count is zero.

Fail-closed errors

Validation failures raise RegistryError, which routes render instead of a traceback.

404 — unknown project id on /api/v1/projects/{project_id}:

{
  "error": "project_not_found",
  "project_id": "not-registered",
  "known_project_ids": ["example"],
  "remediation": "Request one of the known project ids, or add the project ...",
  "source": {"kind": "file", "path": "...", "inventory_complete": true}
}

500 — invalid registry, on both the versioned route and the alias:

{
  "error": "registry_invalid",
  "detail": "unsupported registry version: 42",
  "remediation": "Set 'version' to one of 1, 2 (current schema is 2) ...",
  "field_path": "version",
  "source_path": "/path/to/registry.json"
}

field_path points at the offending location (projects[0].profiles.reconciler, projects[0].onboarding_checklist[2].state, and so on). The HTML routes render the same detail, field, source, and remediation on a "Project registry unavailable" page.

Conditions that fail closed:

  • file missing or unreadable;
  • invalid JSON (the remediation names line and column);
  • root not an object, or projects missing/empty;
  • unsupported version;
  • a credential-shaped key anywhere in the file (token, *_secret, auth_*, and similar);
  • a project missing a required field, or missing an author/reviewer/reconciler profile;
  • an unknown status, onboarding state, or health status.

Credential rule

The registry stores redacted metadata only. Credential-shaped keys are rejected at load time, before any DTO is built, consistent with safety-model.md and credential-isolation.md. Tokens live in the keychain and are resolved server-side by gitea_auth.

Migrating a version 1 registry

  1. Set "version": 2.
  2. Optionally add "status" per project (omitted means active).
  3. Optionally add "remote_name" per project.
  4. Optionally add "state" and "required" to each onboarding step (omitted means pending and true).
  5. Optionally add "last_seen_health".

No step is mandatory: a version 1 file keeps loading. Bumping the version only declares that the file may use the v2 fields.