Surface operational signal alongside inventory: upcoming-EOL banner and
KPI for everyone; admin-only repairs tempo, FM close time, open FMs by
host, and custody backlog. Service shapes payload by role.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Clicking a category anywhere in the app now opens /categories/:id with
MPN breakdown, manufacturer mix, failures by MPN, and past-EOL exposure
— a dual of the manufacturer detail page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds /manufacturers/:id with vendor-wide KPIs, top MPNs by units,
failures by MPN, category mix, past-EOL exposure, and a filtered
PartModels table. Wires upstream links from PartDetail and
PartModelDetail so the manufacturer name is a navigable anchor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds /part-models/:id mirroring host/part detail pattern: KPIs for
units, spend, avg price, failure counts, and FMs implicating the
model, a state-breakdown bar chart, and the parts-of-this-model
table. New GET /part-models/:id/insights aggregates via part.groupBy
+ aggregate and repair/fm counts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a Generate button to the host create dialog that fetches a random
8-digit asset ID from the new GET /hosts/generate-asset-id endpoint.
The service retries against the unique index so the returned ID is
guaranteed unused. Edit mode hides the button.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add /hosts/:id detail page with unified timeline (HostEvents + FMs + Repairs
+ part arrivals/departures) and a deployed-parts table. Hosts list rows now
link to the page. FM list + detail surface inline State/Stack badges next
to the asset ID, with the asset ID linking to the host page.
HostEvent audit model added; create/update in the hosts service now diff
and log state, stack, and field changes the same way parts.ts does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Seven bundled improvements:
- PartModel combobox on Add Part + Log Repair (known MPN auto-fills;
unknown reveals manufacturer picker for catalog upsert).
- Host lifecycle: state (DEPLOYED/DEGRADED/TESTING) and stack
(PRODUCTION/VETTING) fields, driven by external clients via the API.
- Locations page redesigned as a 2-pane tree + bin grid with breadcrumb.
- PENDING_REPAIR custody state: tech takes a SPARE into custody for a
future swap; resolves to DEPLOYED via Repair or back to SPARE via a
bin-required drop-off.
- Move Category from Part to PartModel; seed common categories
(GPU/RAM/SSD/HDD/NIC/CPU/PSU/MOBO). Parts table gets a Category
column and filter sourced from the model.
- Fix Deployed Value 100x bug on the Dashboard (price is stored as
dollars, not cents).
- PartModels table shows "No" instead of "--" when destroyOnFail=false.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The old Repairs module had grown ticketing-system features (status lifecycle,
comments, assignee, notes) that duplicate what the external ticketing tool
already owns. Vector only needs to track whether maintenance is open or closed.
- Rename RepairJob -> Fm (OPEN/CLOSED only), drop RepairComment, assignee, notes
- New Repair table: persistent log of physical part swaps, with ingest on
unknown broken MPN via partModels.upsertByMpn
- New custody model: PENDING_DROP_IN_CUSTODY / PENDING_DESTRUCTION_IN_CUSTODY
states + Part.custodianId, with a "My Custody" page for drop-off
- PartModel.destroyOnFail routes broken parts to the destruction path
- Host lookup on /fms and /repairs accepts hostId XOR assetId
- Wire the dormant webhook emitter: fm.opened, fm.closed, repair.logged
- Single fresh Prisma migration (dev DB was wiped, no backfill)
Tests: 60 passing (custody transitions in parts.test.ts; new fms.test.ts,
repairs.test.ts, custody.test.ts covering happy paths, validation failures,
webhook emissions, and ingest-on-unknown-MPN).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DEPLOYED parts live on a host; every other state lives in a bin (or
unassigned). Previously binId and hostId were independent nullable
fields with no validation, so the Edit Part dialog could leave a
DEPLOYED part with only a bin and no host — which silently dropped
it from the repair problem-part picker.
- Service: resolveLocation() helper enforces the invariant on create
and update. On a state transition, update auto-clears the stale
relation and emits LOCATION_CHANGED for the cleared side.
- Zod: CreatePartRequest.superRefine rejects mismatched state/location
up front; UpdatePartRequest rejects both-fields-set.
- Web: PartFormDialog swaps a single Location field between Host
combobox (DEPLOYED) and Bin combobox (others); switching State
clears the opposite field. Parts list + detail render host first,
then bin path, then Unassigned.
- Tests: 9 new cases covering the invariant including the no-op guard
so an unrelated PATCH on a DEPLOYED part doesn't touch hostId/binId.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four domain-model changes driven by exercising the deployed 2.0 build:
- EOL moves from manufacturer to MPN via new PartModel catalog table,
so alerts fire on the thing that actually ages.
- Repairs re-home to Host (required hostId + problem text) with an
optional RepairJobPart join for affected parts; drop Part.replacementPartId.
- New /repairs/:id detail page with editable problem, part list, and
a RepairComment thread (REPAIR_COMMENTED events fan out to each
problem part's timeline).
- Host.assetId (required, unique) surfaces prominently on the repair
page so techs can confirm they're touching the right box.
Single destructive migration reshapes existing dev data. All 7 packages
typecheck clean; 30 API tests pass (9 new covering host membership,
upsertByMpn idempotency + race, assetId 409, comment userId stamping).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Every cookie was flagged Secure whenever NODE_ENV=production. Over
plain HTTP (single-host compose deploy without TLS) browsers silently
discard Secure cookies, so the access token, refresh token, and CSRF
cookie all vanished after login — producing 401 Unauthorized on every
GET and 403 "CSRF token missing or invalid" on every mutation.
Add COOKIE_SECURE to ApiEnv: optional boolean, falls back to
NODE_ENV === 'production' when unset. Controllers and middleware now
read env.COOKIE_SECURE instead of the NODE_ENV shortcut. The compose
file sets it to false by default with a comment to flip once TLS is in
front; HTTPS deployments can override via .env or drop the override to
pick up the secure default.
Two bugs kept a fresh docker-compose deploy from ever accepting admin:admin:
1. resolveSqliteUrl in packages/db/src/client.ts stripped leading slashes
wholesale — so file:/data/vector.db became a relative path and was
resolved against packages/db/prisma/. Prisma CLI (migrate deploy)
correctly wrote to /data/vector.db on the mounted volume; the app's
runtime client connected to an empty file at packages/db/prisma/data/
vector.db with no tables, so login threw. The helper now passes Unix
absolute paths through verbatim, still normalizes file:/// triple-
slash URLs, and only resolves truly relative paths against the schema
dir.
2. The Dockerfile CMD ran migrations but not a seed, so even when the
path bug is fixed the User table is empty — admin:admin 401s forever.
Added packages/db/ensure-admin.mjs (pure JS, no tsx needed) that
creates the default admin user iff User.count() === 0, and wired it
into the API CMD between migrate deploy and node. Credentials can be
overridden with SEED_ADMIN_{USERNAME,PASSWORD,EMAIL}.
packages/db/src/index.ts re-exports model types from @prisma/client,
so the generated client has to exist before tsc walks that file. The
previous order hit TS2305 on User/Manufacturer/Site/etc.
CI was failing because only ~7% of services/lib is covered today — the
60% threshold was aspirational, not grounded in what ships. Keep the
v8 report + artifact upload so contributors can see the trend; add a
threshold back once service-level coverage catches up.
- apps/api/Dockerfile: multi-stage build, runs prisma migrate deploy on
boot. Workspace package.json "main/exports" rewritten to dist so Node
ESM resolves compiled JS at runtime.
- apps/web/Dockerfile + nginx.conf: static build served by nginx with
SPA fallback, gzip, cache-bust on hashed assets, and /api reverse
proxy to the internal api service.
- docker-compose.yml: production-oriented stack — api (SQLite on a
named volume), web (exposes WEB_PORT), redis (for the upcoming
worker). Postgres dropped since schema still targets SQLite.
- .dockerignore: keep build context lean.
- ci: add docker job gated on push-to-main that builds and pushes both
images to ${{ vars.REGISTRY_URL }} using ${{ secrets.REGISTRY_TOKEN }}.
Tags :latest + :${github.sha}.