feat: split Repairs into FM, Repair, and Custody workflows
CI / Lint · Typecheck · Test · Build (push) Successful in 44s
CI / Playwright (smoke) (push) Has been skipped
CI / Build & push images (push) Successful in 1m0s

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>
This commit is contained in:
2026-04-17 12:22:56 -04:00
parent 6690d8a5dd
commit 3d77f2846d
54 changed files with 3304 additions and 1287 deletions
+10 -4
View File
@@ -116,7 +116,7 @@ export default function Parts() {
id: 'location',
header: 'Location',
cell: ({ row }) => {
const host = row.original.host;
const { host, custodian, bin } = row.original;
if (host) {
return (
<span className="text-xs font-mono text-muted-foreground">
@@ -124,9 +124,15 @@ export default function Parts() {
</span>
);
}
const path = row.original.bin?.fullPath;
return path ? (
<span className="text-xs font-mono text-muted-foreground">{path}</span>
if (custodian) {
return (
<span className="text-xs text-muted-foreground">
Custody: {custodian.username}
</span>
);
}
return bin?.fullPath ? (
<span className="text-xs font-mono text-muted-foreground">{bin.fullPath}</span>
) : (
<span className="text-xs text-muted-foreground italic">Unassigned</span>
);