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>
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}.
Lock images to gitea.thewrightserver.net/josh/{vector-api,vector-web}
and drop the build: sections. docker compose up now only pulls; source
builds happen exclusively in CI.
Gitea's OCI registry requires <host>/<owner>/<image>. Pushes to the
bare <host>/<image> path return 404. Prepend github.repository_owner
so REGISTRY_URL can stay as just the hostname.
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.
Gitea Actions rejects @actions/artifact v2.0+ (upload-artifact@v4,
download-artifact@v4) with a GHESNotSupportedError. v3 is the highest
supported on current Gitea releases.
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}.
The Gitea Actions cache server is unreachable from the runner, so
cache: pnpm hangs ~4m42s on ETIMEDOUT before falling through. Removing
the option drops the step to ~5s; pnpm install on a clean runner is
already fast with the frozen lockfile.
Replace placeholder with a professional README covering architecture,
tech stack, getting-started flow, common tasks, testing, Gitea CI,
conventions, and the nine-phase roadmap.