Conversation
added 15 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.
auth-roadmap.md drops IdP as a WhereTF phase (homelab team owns it) and becomes the 5-phase index for app-side work. deployment.md §Authorization replaces the 2-mode per-org/global split with the isolated/additive/open model and spells out every table's mode. project-intent.md replaces "future: private items" with the concrete ownerOrgId nullable mechanic.
Foundation for app-side multi-tenancy. Auth.js v5 with hybrid providers (email+password credentials, optional homelab OIDC, dev impersonate). New users/orgs/user_orgs tables via migration 0015. Every additive + isolated data table gains ownerOrgId (nullable, FK to orgs); migration 0016 seeds a default org and backfills isolated rows. NOT NULL flip on isolated tables is deferred to 0017 so the repo layer can migrate first. Session strategy is JWT (Auth.js v5 constraint when Credentials is a provider). Per-request authz hydrates via getRequestContext() — reads orgs/role from DB, falls back to the wtf-active-org cookie. Scope helpers additiveOrgFilter/isolatedOrgFilter build the two filter shapes used by every repo.
Every repository method now takes { userId, orgId, ... } for writes
and { orgId, ... } for reads. Isolated tables filter strictly;
additive tables union global ∪ org via additiveOrgFilter. Writes
default org-private; catalog contributions pass asGlobal: true.
transactionRepository.log carries actor + org on every audit row.
API routes call requireContext() and forward ctx to repos — no route
trusts its own URL for tenancy. The shared requireContext /
errorResponse helpers keep per-route boilerplate tight.
Tests updated to seed a fresh org per beforeEach (testCtx) and
spread it into every repo call. Two-org isolation test per repo
(12 new files) asserts cross-org invisibility on the isolated side
and proper global+own union on the additive side.
seed.ts now looks up the default org/user from the migration seed
and writes catalog rows (items, templates, taxonomy, interface
types) with asGlobal: true so they remain shared across orgs.
Every isolated-mode repo now populates owner_org_id on write (Phase C.2 landed), so the nullable window from migration 0016 is safe to close. 0017 backfills any stragglers to the default org then applies SET NOT NULL on the 6 isolated tables (modules, inserts, locations, assignments, location_interfaces_accepted, transactions). Schema files mirror the constraint with .notNull(). transactionRepository.log tightens userId + orgId back to required — they were loose during the C.2 rollout. Small merge() fix in interfaceTypeRepository uses the active orgId directly when rewriting location junctions rather than preserving a per-holder owner that can no longer be null. 432/432 repo tests still pass.
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.
No description provided.