Replace invalid hsl(var(--accent) / ...) cursors with color-mix against
the real --color-foreground token, and style tooltip content/box with
--color-popover so it matches the dark theme instead of rendering as a
white box on a black cursor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
The broken-model UUID fields used z.string().uuid().optional(), which
only accepts undefined — not the '' defaults. When the broken serial
matched an existing part, those fields unmounted before their
FormMessage could render, so handleSubmit aborted on hidden errors and
the mutation never fired. Accept the empty-string sentinel alongside
UUIDs.
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>
MPN links to /part-models/:id and manufacturer links to
/manufacturers/:id, matching the cross-navigation pattern used on
other detail and table views.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove internal tool references (n8n), product self-references, and
implementation-detail meta from page headers and dialog descriptions.
Copy now describes what the user is looking at rather than how the
system handles it behind the scenes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Clicking a bin on Locations now navigates to /bins/:id, showing the
bin's site/room/name, created/updated metadata, and a paginated
DataTable of parts currently in the bin. Admins can rename or delete
the bin from the detail page; the BinGrid kebab menu still works
without triggering navigation.
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>
The page forced the card to fill the viewport via h-[calc(100vh-...)],
leaving awkward empty space when few bins were present. Drop the fixed
height so the card sizes to its tallest column and let the page scroll
naturally if the bin grid overflows.
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>
- Render State/Stack as plain text in the summary (badges still in header).
- Show FM UUID instead of problem text in the timeline entries.
- Rename PART_ARRIVED label to "Part deployed".
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>
- 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}.