feat(kc): commit extraction pipeline + metadata API (Phase 1)

Implements issue #141 — .kc server-side metadata integration Phase 1.

When a .kc file is uploaded, the server extracts silo/manifest.json and
silo/metadata.json from the ZIP archive and indexes them into the
item_metadata table. Plain .fcstd files continue to work unchanged.
Extraction is best-effort: failures are logged but do not block the upload.

New packages:
- internal/kc: ZIP extraction library (Extract, Manifest, Metadata types)
- internal/db: ItemMetadataRepository (Get, Upsert, UpdateFields,
  UpdateLifecycle, SetTags)

New API endpoints under /api/items/{partNumber}:
- GET    /metadata           — read indexed metadata (viewer)
- PUT    /metadata           — merge fields into JSONB (editor)
- PATCH  /metadata/lifecycle — transition lifecycle state (editor)
- PATCH  /metadata/tags      — add/remove tags (editor)

SSE events: metadata.updated, metadata.lifecycle, metadata.tags

Lifecycle transitions (Phase 1): draft→review→released→obsolete,
review→draft (reject).

Closes #141
This commit is contained in:
Forbes
2026-02-18 16:37:39 -06:00
parent d96ba8d394
commit dd010331c0
7 changed files with 885 additions and 0 deletions

View File

@@ -53,6 +53,7 @@ type Server struct {
modules *modules.Registry
cfg *config.Config
settings *db.SettingsRepository
metadata *db.ItemMetadataRepository
}
// NewServer creates a new API server.
@@ -81,6 +82,7 @@ func NewServer(
jobs := db.NewJobRepository(database)
settings := db.NewSettingsRepository(database)
locations := db.NewLocationRepository(database)
metadata := db.NewItemMetadataRepository(database)
seqStore := &dbSequenceStore{db: database, schemas: schemas}
partgen := partnum.NewGenerator(schemas, seqStore)
@@ -109,6 +111,7 @@ func NewServer(
modules: registry,
cfg: cfg,
settings: settings,
metadata: metadata,
}
}
@@ -1652,6 +1655,9 @@ func (s *Server) HandleUploadFile(w http.ResponseWriter, r *http.Request) {
Int64("size", result.Size).
Msg("file uploaded")
// .kc metadata extraction (best-effort)
s.extractKCMetadata(ctx, item, fileKey, rev)
writeJSON(w, http.StatusCreated, revisionToResponse(rev))
}