Interface type management: admin CRUD, lifecycle, junctions, merge#5
Merged
Interface type management: admin CRUD, lifecycle, junctions, merge#5
Conversation
added 8 commits
April 17, 2026 23:09
Restore the WhereTF map pin logo to the sidebar header, add it as an SVG favicon (both app/icon.svg route and public/ static fallback), and replace all plain "Loading..." text with an animated pin spinner. Remove unused default Next.js boilerplate SVGs from public/. Fix stale .env.local.example (was referencing MongoDB/OAuth).
Spec covers admin CRUD, template+receptacle integration, placement check (interface match + dimensional fit), lifecycle (archive/merge/ delete), maturity states, and optional unit-system convenience layer. Prototype shows list view with filter tabs, bulk merge flow, detail panel with per-axis unit editor, archive/delete lifecycle menu, and chip preview.
Maturity radio in the edit form was too prominent. Remove it entirely. Default new interfaces to stable; "Save as draft" secondary button is the explicit opt-in to draft. Edit form for stable types has no demotion path — state machine is one-way (draft → stable → archived). Detail header shows a small draft pill when viewing a draft.
Slice 1 of interface-type-management implementation — data layer only, no UI yet. Extends interface_types with maturity (draft|stable), archived_at timestamp, and unit_system jsonb. Adds junction tables for template_version × interface_type (provided + accepted) and location × interface_type (accepted). Old single-text columns stay in place and authoritative for now; slice 2 migrates template/location repos onto the junctions. Repository gains archive / unarchive / usageCount. Update enforces one-way maturity state machine — stable is terminal. Remove requires archived + zero usage. List supports status=active|archived|all. API: new POST /api/interface-types/:id/archive and /unarchive routes. Existing PATCH returns 409 on stable→draft demotion. DELETE returns 409 until archived and unused. GET detail includes usage counts. 40 repo tests pass; end-to-end smoke test via curl covers the full lifecycle.
Slice 2 of interface-type-management. Single-text columns on template
versions, locations, and inserts are replaced by UUID-keyed junction
tables. Backfill resolves identifiers to UUIDs in migration 0014 then
drops the old columns. template_versions.unit_size (the "42mm" text
field) also drops — unit info now lives on interface_types.unit_system.
Repositories now take UUID arrays for interface membership:
templateRepository.create / publishVersion accept
interfacesProvidedIds[] and interfacesAcceptedIds[]
locationRepository.create accepts interfacesAcceptedIds[]
insertRepository drops the interfaceTypeProvided param — inserts
inherit provided interfaces from their template version per spec
(no per-insert override)
New helpers: templateRepository.getVersionInterfaces /
setVersionInterfaces, locationRepository.getAcceptedInterfaces /
setAcceptedInterfaces / getAcceptedInterfacesByLocationIds.
Placement check is now set intersection over UUIDs. Still optimistic
when either side is empty (user rules the storage).
API: /api/locations GETs now include interfacesAccepted on each
location; PATCH accepts interfacesAcceptedIds to drive
setAcceptedInterfaces. /api/inserts list filter renamed to
interfaceTypeId (UUID) and listWithDetails returns
interfaceTypes: [{ id, identifier }].
Seed rewritten to use repo helpers and pass UUIDs. Full test suite
passes (397 tests).
Pages (app/inserts, app/modules) still reference the old single-string
shape and will get stale data until slice 4 rebuilds the chip UI.
Slice 4 of interface-type-management. New route /admin/interfaces is the admin surface for managing interface types. List view on the left (checkbox + identifier + maturity badge + status badge + description + usage counts + created date + row actions) with Active / Archived / All filter tabs and a bulk-actions bar. Detail pane on the right opens on row click or the "New Interface Type" header button. Detail form mirrors the prototype: identifier (mutable slug), multi- line description, unit-system toggle with per-axis label + mm input, free-form physical-contract notes, live usage panel. Derive-new button clones description + contract + unit system into a fresh draft. Save button behavior follows the spec state machine: Create mode: "Save as draft" + "Create" (stable default) Edit draft: "Save as draft" + "Save" (promotes to stable) Edit stable: single "Save" — no demotion path Lifecycle menu in the header handles Archive / Unarchive / Delete. Delete is gated on archived + 0 usage; the menu surfaces the reason when disabled. Archive row action mirrors the same logic in-line. Sidebar gains an Interfaces entry under the Admin section so the route is discoverable. Merge (bulk action) is stubbed with a disabled button — lands in the final slice alongside its backend.
Final slice of interface-type-management. The merge operation
consolidates N source interface types into a single target, in one
transaction:
1. Rewrite template_version_interfaces_{provided,accepted} junctions
from source IDs to target ID, deduping on conflict so a template
version that already holds target doesn't get a duplicate row.
2. Rewrite location_interfaces_accepted the same way.
3. Mint a new template version per affected template, cloning the
latest affected version's structure and carrying the now-remapped
junctions forward — interfaces-provided/accepted are versioned
properties per spec, so a merge must be a versioned event.
4. Hard-delete the source interface_types rows (merge bypasses the
archive-gate; tombstoning is part of the consolidation).
Errors: empty sourceIds (400), target in sources (409), target or
source not found (404).
API: POST /api/interface-types/merge with { sourceIds, targetId }
returns { referencesUpdated, templateVersionsMinted, sourcesDeleted }.
Admin page: "Merge into…" bulk action opens a modal showing the
selected sources, total reference count, and a radio-pick survivor
list (active non-selected types, with usage ref counts). Destructive
warning surfaces the ref + version mint counts. Execute refreshes the
list, closes the detail pane if a source was active, and toasts the
summary.
Interface-type-management work is now complete (slices 1–4).
50 repo tests pass; smoke-tested end-to-end via curl.
Slice 2 dropped the single-text interface_type columns and moved
membership to junction tables, but the /inserts, /modules, and
/modules/new pages were still reading ins.interfaceType (string) and
level.interfaceTypeAccepted (string) and sending those names back in
mutation bodies. The fields no longer exist, so data was stale and
mutations silently dropped.
All four pages now use the new shape:
- Location.interfacesAccepted: Array<{ id, identifier }>
- Insert.interfaceTypes: Array<{ id, identifier }>
- Level form state switches from interfaceTypeAccepted: string to
interfaceTypeId: string (single-select UUID; multi-select chips
are a later slice)
- Query params and PATCH/POST bodies use interfaceTypeId /
interfacesAcceptedIds (UUIDs), never identifier strings
- Dropdowns key by UUID id, display identifier
- Chip rendering joins identifiers when a level accepts or an
insert provides multiple
templateRepository.listWithCurrentVersion now batch-loads provided
and accepted interface identifiers onto currentVersionData so the
module page can filter candidate templates against a receptacle's
accepted interface without a second round-trip.
No schema change. All 407 tests pass. Smoke-tested /modules,
/modules/new, /modules/<id>, /inserts, /admin/interfaces — all return
200 and the API responses carry the new array shape.
TS2322: setDraft is SetStateAction<FormDraft | null>, but the DetailForm prop declared the narrower (FormDraft | (FormDraft) => FormDraft) => void, which an updater that returns null couldn't satisfy. Use React's SetStateAction<FormDraft | null> directly so the prop signature matches the useState setter exactly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces a full admin surface for interface types — the named compatibility classes that govern insert ↔ receptacle placement. Seven commits, four implementation slices plus spec/prototype, green across 407 tests.
Prior to this PR, interface types were a thin
interface_typestable and single-text columns ontemplate_versions,locations, andinserts. After this PR:maturity(draft | stable),archived_at, andunit_system(per-axis modular convenience units). Identifier is a mutable display slug; all references are by UUID.template_version_interfaces_{provided,accepted}andlocation_interfaces_accepted. Junctions support multi-membership per spec (e.g., an Akro-Mils bin providing bothlouver-hangandopen-surface)./admin/interfacespage provides list, filter tabs (active/archived/all), detail form with per-axis unit editor, lifecycle menu (archive/unarchive/delete with gate), and a bulk merge modal with survivor picker./inserts,/modules,/modules/new,/modules/<id>) updated to read/write the new array shape.(Branch is named
logo-integrationfor historical reasons — the logo commit itself already merged to main via a separate path; every commit here is interface-type work.)Spec
Full design at specification/interface-type-management.md. Built via WhereTF's 5-phase UX dev pattern (specify → survey → converge → prototype → implement), with prototype at prototypes/interfaces-v1.html.
Commits
dc182c3— spec + prototype v108103e4— prototype maturity UX simplification (drop form radio, defer to save button)16797fe— slice 1: lifecycle columns + junction tables + archive/unarchive + one-way maturity guardf952972— slice 2: repos migrated onto junctions, old text columns dropped (backfill in migration 0014)e835649— slice 4:/admin/interfaceslist + detail + CRUD UI317bc85— slice 3: merge repo + API + UI6943295— consumer pages updated to new shapeKey design decisions
draft → stable → archived → deleted). No demotion path. If a stable type is wrong, rename/merge/archive it.Schema changes
Two new migrations. Both idempotent; apply cleanly to a fresh DB.
0013_interface_type_lifecycle.sql— addsmaturity,archived_at,unit_systemcolumns and the three junction tables.0014_interface_type_junctions_migrate.sql— backfills the junction tables from the old text columns, then drops them along with the deprecatedtemplate_versions.unit_size.No
insert_interfaces_providedtable; inserts inherit from template.Deployment notes
drizzle.__drizzle_migrationswas only tracking 0000-0005 pre-PR; 0006-0014 were in the DB but unrecorded. Backfilled on dev + test DBs, sodrizzle-kit migratenow runs cleanly.drizzle.__drizzle_migrationswith the missing hashes before running the deploy.Test plan
/admin/interfaceslist, detail, create, edit, archive, unarchive, delete, merge modalKnown follow-ups (not blocking this PR)
locationNeighborsRepository.getAdjacentInDirection) — spec §"Cross-location overflow helper".isAdmin({ userId })helper shape — API routes gate nothing today. Stub for when multi-tenant auth lands.