Lets an Admin/Super-Admin review pending catalog submissions and approve
(in place, same id) or reject (with a required reason) them, per
organized-ideas.md §3/§9.
Backend (supabase/migrations/20260910010000_admin_review_queue.sql):
- Per-user pending-submission cap (10), enforced in catalog_submissions'
insert policy rather than trusted to the client.
- catalog_entity_usage_impact(entity_type, entity_id): a SECURITY DEFINER,
admin-gated aggregate function answering "how many diagrams reference
this, and a short sample" by scanning diagrams.data JSONB — never raw
diagram content, and available to regular Admins even though they don't
otherwise have diagram visibility (only Super Admins do, per §6).
- catalog_submission_submitters(ids[]): same admin-gated pattern, batched,
so the queue can show who submitted something without opening general
profile browsing to regular Admins.
- Follow-up migration: a rejected submission had no way out (the delete
policy only allowed withdrawing 'pending') — extended to allow 'rejected'
too, so a submitter can dismiss one they don't intend to revise.
- 12 new pgTAP tests (38/38 total) covering the cap, both privileged
functions (including the non-admin-gets-rejected case), and withdrawing
pending vs. rejected submissions.
Frontend:
- authStore: minimal role awareness, replacing TopBar's local username
fetch, used to gate the Review Queue UI.
- AdminSubmissionRepository/SupabaseAdminSubmissionRepository +
adminReviewStore: list all submissions, approve/reject, usage impact,
submitter usernames.
- AdminReviewModal: per-submission diff view (current vs. proposed, both
row-shaped via the existing catalog<->row mappers), a duplicate-detection
nudge (Levenshtein distance against existing public device names) for
new device submissions, and an inline impact-check for edits to
already-public entries before approving.
- TopBar: role-gated "Review Queue" button with a pending-count badge; "My
Submissions" gets an unseen-outcome badge (localStorage-tracked, like the
existing hidden-template preference) so a submitter notices a decision
without having to keep reopening the modal.
- Deliberately deferred: the site-wide announcement banner (its own
follow-up, per discussion) and the Admin/Super-Admin role-assignment UI
(§9's later phase — becoming an Admin locally still means setting
profiles.role via SQL/Studio).
Verified: tsc -b and oxlint clean; supabase db reset + 38/38 pgTAP tests
pass; confirmed the two new RPC functions are actually reachable through
PostgREST (not just raw SQL) via a live curl call; manually tested
submit -> review -> approve/reject -> (for rejected) dismiss end to end.
Lets users submit a private catalog entry (port type, cable type, device
template) for promotion to the public catalog, or suggest an edit to an
existing public entry — both go into the catalog_submissions review queue
per organized-ideas.md §3. No Admin review UI yet (next sub-phase); this
covers the submitter's side only.
- data/SubmissionRepository + SupabaseSubmissionRepository: submit,
resubmit, withdraw, list-mine, backed by the existing catalog_submissions
RLS policies (no schema changes needed).
- data/catalogRowMapping.ts: shared domain<->row mappers, in both
directions, so a submission's proposed_data is always shaped like the
underlying table row (what an eventual admin-approval would write
directly) and can be turned back into form-editable fields for revision.
- state/submissionStore.ts: mySubmissions + submit/resubmit/withdraw, plus
syncProposedData — called from catalogStore's updateCustom* actions so a
submission about your own still-private entry never goes stale relative
to it (edits from the library and from My Submissions are the same
action and always agree).
- UI: "Submit"/"Suggest edit" wired into PortTypeManager, CableTypeManager,
DevicePalette/DeviceTemplateEditor; new MySubmissionsModal (opened from
TopBar, with a pending-count badge) shows status, rejection reasons, and
lets you edit/resubmit or withdraw.
- Deliberately deferred: device_category submissions (no listing UI to
hang a button on yet) and the normalized manufacturer catalog.
Verified: tsc -b and oxlint clean; supabase db reset + 23/23 pgTAP RLS
tests still pass (no schema changes this round); manually tested submit,
suggest-edit, edit-from-either-side sync, reject/resubmit, and withdraw.
Moves port/cable/category/device-template data from per-diagram embedded
storage to the global public/private catalog backed by Supabase, per
organized-ideas.md's "live reference, not snapshot" decision.
- domain/types.ts, project.ts, compatibility.ts, bom.ts: lookup functions
now take an explicit Catalog parameter instead of deriving data from
Project — Project is reduced to just diagram-scoped fields.
- New CatalogRepository/SupabaseCatalogRepository (mirrors the
DiagramRepository pattern) and catalogStore.ts, replacing the
catalog-related actions that used to live in projectStore.
- device_templates gets a plain-text manufacturer column for now (the
normalized manufacturer catalog from organized-ideas.md §3 is its own
future pass, not blocking this one).
- domain/library.ts is no longer imported by the app — it's now only the
source scripts/generate-seed.mjs reads to produce supabase/seed.sql.
- Swept every UI call site via tsc -b until clean; oxlint clean; 23/23
pgTAP RLS tests still passing after a `supabase db reset`.
- DiagramRepository interface redesigned around multiple diagrams:
list()/loadById()/deleteById() replace the old single-diagram
load()/clear(). Both implementations updated to match.
- SupabaseDiagramRepository.save() now does an explicit update-or-insert
instead of a blind upsert, so owner_id is only ever set at creation --
an upsert would resend it on every save and let whoever saves last
silently reassign ownership. Not reachable yet (no collaborator UI),
but a real landmine once diagram sharing (organized-ideas.md §8) lands,
and cheap to avoid now.
- LocalStorageDiagramRepository now stores diagrams keyed by id (was a
single fixed key), keeping it a genuine working fallback rather than
a stale reference implementing an old interface.
- Store: loadInitialDiagram (renamed from loadFromStorage) opens the
last diagram you had open (tracked in localStorage -- a UI
preference, not app data), falling back to the most recently updated
one, falling back to a fresh empty diagram. New actions:
refreshDiagramList, switchToDiagram, deleteDiagram. newProject and
importProject now persist immediately (not just via the debounced
autosave) so a new/imported diagram shows up in the list right away;
importProject also assigns a fresh id so it can't collide with an
existing diagram.
- New DiagramManagerModal (list/open/delete/+New), opened from a
"Diagrams" button in TopBar that replaces the old single-diagram
"New" button and its now-unnecessary confirmation dialog -- nothing
is lost by creating a new diagram anymore, since the old one stays
saved and reachable from the list.
Verified insert/list/update(-preserves-owner)/loadById/delete against
the real local stack; RLS test suite still 23/23 after a fresh reset.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x