# Central DB — ER diagram Schema for the `central/` verification dashboard (Postgres). Source of truth: [`central/db/init.sql`](../db/init.sql) (re-run idempotently on every server boot). This diagram was generated from the **live** database and verified against it. > Render: GitHub renders the Mermaid block below directly; in VS Code use the > *Markdown Preview Mermaid* extension (or any Mermaid viewer). ## Hierarchy at a glance ``` clients ──1:N──▶ projects ──1:N──▶ videos ──1:N──▶ annotations ├──1:N──▶ video_remarks (remark thread) └──1:N──▶ video_events (claim/push log) users — provisioned workers/admins (token auth) audit_events — org-level action log (both link to the hierarchy only "softly", by username string — no FK) ``` ## Entity–relationship diagram ```mermaid erDiagram clients ||--o{ projects : "owns" projects ||--o{ videos : "contains" videos ||--o{ annotations : "has" videos ||--o{ video_remarks : "thread" videos ||--o{ video_events : "logs" clients { int id PK text name UK "unique client name" timestamptz created_at } projects { int id PK int client_id FK "→ clients.id (CASCADE); nullable" text name "UK(client_id, name)" text source_path "NAS export path only — host lives in NFS_HOST env" text source_kind "'local' | 'nfs'" timestamptz created_at } videos { int id PK "= serverVideoId on the desktop" int project_id FK "→ projects.id (CASCADE)" text file_name text rel_path "UK(project_id, rel_path); keeps subfolders" bool has_json "sibling export-JSON exists" int width int height float8 fps int frame_count int annotation_count bigint annotation_time_ms text primary_annotator text status "pending | annotated | verified" timestamptz annotated_at "max(annotation.created_at)" jsonb raw_json "full imported export doc (Phase-3 pull)" timestamptz ingested_at text claimed_by "claim dimension — orthogonal to status" timestamptz claimed_at timestamptz lease_expires_at text completed_by "set on push (verify pass)" timestamptz completed_at bigint verify_time_ms } annotations { int id PK int video_id FK "→ videos.id (CASCADE)" int frame_number "0-indexed" text label text side text shape_type "bbox | polygon" jsonb vertices text annotated_by "username (soft link to users)" text review_status "default 'none'" text remark "per-annotation note (NOT the thread)" text subclass text created_at } video_remarks { int id PK int video_id FK "→ videos.id (CASCADE)" text username "author (soft link to users)" text body timestamptz created_at } video_events { int id PK int video_id FK "→ videos.id (CASCADE)" text username text event "claim | release | push | auto_release | reopen" timestamptz at jsonb meta } users { int id PK text username UK text display_name text token_hash "SHA-256 of issued token (never clear)" text role "worker | admin" bool active timestamptz created_at } audit_events { int id PK timestamptz at text username "actor" text action "client_created | project_created | project_source_changed | synced | user_created | user_token_reset" jsonb detail } ``` ## Relationships | Parent | Child | Cardinality | On delete | Constraint | |---|---|---|---|---| | `clients.id` | `projects.client_id` | 1 : N | CASCADE | FK | | `projects.id` | `videos.project_id` | 1 : N | CASCADE | FK | | `videos.id` | `annotations.video_id` | 1 : N | CASCADE | FK | | `videos.id` | `video_remarks.video_id` | 1 : N | CASCADE | FK | | `videos.id` | `video_events.video_id` | 1 : N | CASCADE | FK | **Soft links (by `username` string — no FK):** `users.username` is referenced as text by `annotations.annotated_by`, `videos.claimed_by` / `completed_by` / `primary_annotator`, `video_remarks.username`, `video_events.username`, and `audit_events.username`. These are intentionally not foreign keys so historical attribution survives a user being removed. ## Unique constraints & key indexes - **Unique:** `clients(name)`, `projects(client_id, name)`, `videos(project_id, rel_path)`, `users(username)`. - **Indexes:** `idx_videos_project`, `idx_videos_status(project_id,status)`, `idx_videos_claim(project_id,claimed_by,lease_expires_at)`, `idx_annotations_video`, `idx_annotations_user`, `idx_users_token`, `idx_video_remarks_video(video_id,created_at)`, `idx_events_video`, `idx_events_at`, `idx_audit_at`, `idx_projects_client`. ## Notes - **Two orthogonal dimensions on `videos`:** annotation `status` (`pending`/`annotated`/`verified`) vs. the claim/lease columns (`claimed_by` + `lease_expires_at`). Claimable = `status IN ('pending','annotated')` and unclaimed-or-lease-expired; push → `verified` + `completed_by`/`verify_time_ms`. - **A range annotation** in the export doc expands to **1–2 rows** here (start frame, and end frame if present). - The NAS host/IP is never persisted — `projects.source_path` holds only the export path; the host lives in the `NFS_HOST` server env. - Everything cascades from `clients`: deleting a client removes its projects → videos → annotations/remarks/events. --- ### Regenerate / verify against the live DB ```bash # columns docker exec central-db-1 psql -U central -d central -c "\d+ videos" # foreign keys docker exec central-db-1 psql -U central -d central -Atc " SELECT tc.table_name, kcu.column_name, ccu.table_name, rc.delete_rule FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu USING (constraint_name) JOIN information_schema.constraint_column_usage ccu USING (constraint_name) JOIN information_schema.referential_constraints rc USING (constraint_name) WHERE tc.constraint_type='FOREIGN KEY';" ```