feat: remove FM feature from Vector
FMs move to a separate application. Drops Fm/FmPart tables + Repair.fmId column, deletes FM_OPENED/FM_CLOSED PartEvent rows, strips FM enums + webhook events + shared contracts, removes FM routes/services/pages/UI, and collapses dashboard admin ops to Repairs 7d/30d + trend + custody backlog. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `fm_parts` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `fms` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the column `fmId` on the `repairs` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- Drop orphan part_events referencing the retired FM event types.
|
||||
DELETE FROM "part_events" WHERE "type" IN ('FM_OPENED', 'FM_CLOSED');
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "fm_parts_partId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "fms_status_openedAt_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "fms_hostId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "fms_status_idx";
|
||||
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "fm_parts";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "fms";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_repairs" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"hostId" TEXT NOT NULL,
|
||||
"brokenPartId" TEXT NOT NULL,
|
||||
"replacementPartId" TEXT NOT NULL,
|
||||
"performedById" TEXT NOT NULL,
|
||||
"performedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "repairs_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "repairs_brokenPartId_fkey" FOREIGN KEY ("brokenPartId") REFERENCES "Part" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "repairs_replacementPartId_fkey" FOREIGN KEY ("replacementPartId") REFERENCES "Part" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "repairs_performedById_fkey" FOREIGN KEY ("performedById") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_repairs" ("brokenPartId", "createdAt", "hostId", "id", "performedAt", "performedById", "replacementPartId", "updatedAt") SELECT "brokenPartId", "createdAt", "hostId", "id", "performedAt", "performedById", "replacementPartId", "updatedAt" FROM "repairs";
|
||||
DROP TABLE "repairs";
|
||||
ALTER TABLE "new_repairs" RENAME TO "repairs";
|
||||
CREATE INDEX "repairs_hostId_performedAt_idx" ON "repairs"("hostId", "performedAt" DESC);
|
||||
CREATE INDEX "repairs_performedById_performedAt_idx" ON "repairs"("performedById", "performedAt" DESC);
|
||||
CREATE INDEX "repairs_brokenPartId_idx" ON "repairs"("brokenPartId");
|
||||
CREATE INDEX "repairs_replacementPartId_idx" ON "repairs"("replacementPartId");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -138,7 +138,6 @@ model Part {
|
||||
updatedAt DateTime @updatedAt
|
||||
events PartEvent[]
|
||||
tags PartTag[]
|
||||
problemInFms FmPart[]
|
||||
brokenRepairs Repair[] @relation("BrokenRepairs")
|
||||
replacementRepairs Repair[] @relation("ReplacementRepairs")
|
||||
|
||||
@@ -197,7 +196,6 @@ model Host {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
parts Part[]
|
||||
fms Fm[]
|
||||
repairs Repair[]
|
||||
events HostEvent[]
|
||||
|
||||
@@ -221,37 +219,6 @@ model HostEvent {
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Fm {
|
||||
id String @id @default(uuid())
|
||||
hostId String
|
||||
host Host @relation(fields: [hostId], references: [id], onDelete: Restrict)
|
||||
status String @default("OPEN")
|
||||
problem String
|
||||
openedAt DateTime @default(now())
|
||||
closedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
problemParts FmPart[]
|
||||
repairs Repair[]
|
||||
|
||||
@@index([status])
|
||||
@@index([hostId])
|
||||
@@index([status, openedAt(sort: Desc)])
|
||||
@@map("fms")
|
||||
}
|
||||
|
||||
model FmPart {
|
||||
fmId String
|
||||
partId String
|
||||
fm Fm @relation(fields: [fmId], references: [id], onDelete: Cascade)
|
||||
part Part @relation(fields: [partId], references: [id], onDelete: Restrict)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@id([fmId, partId])
|
||||
@@index([partId])
|
||||
@@map("fm_parts")
|
||||
}
|
||||
|
||||
model Repair {
|
||||
id String @id @default(uuid())
|
||||
hostId String
|
||||
@@ -263,13 +230,10 @@ model Repair {
|
||||
performedById String
|
||||
performedBy User @relation(fields: [performedById], references: [id], onDelete: Restrict)
|
||||
performedAt DateTime @default(now())
|
||||
fmId String?
|
||||
fm Fm? @relation(fields: [fmId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([hostId, performedAt(sort: Desc)])
|
||||
@@index([fmId])
|
||||
@@index([performedById, performedAt(sort: Desc)])
|
||||
@@index([brokenPartId])
|
||||
@@index([replacementPartId])
|
||||
|
||||
@@ -30,10 +30,7 @@ export interface PartModelEolSummary {
|
||||
export interface OperationsAnalytics {
|
||||
repairs7d: number;
|
||||
repairs30d: number;
|
||||
newFms7d: number;
|
||||
avgFmCloseHours30d: number | null;
|
||||
repairsTrend30d: { date: string; count: number }[];
|
||||
openFmsByHost: { hostId: string; hostName: string; count: number }[];
|
||||
custodyBacklog: { userId: string; username: string; count: number }[];
|
||||
}
|
||||
|
||||
@@ -44,6 +41,5 @@ export interface DashboardAnalytics {
|
||||
topBins: BinCount[];
|
||||
deployedPastEol: PartModelEolSummary[];
|
||||
upcomingEol: PartModelEolSummary[];
|
||||
openFms: number;
|
||||
operations?: OperationsAnalytics;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ export const PartEventType = z.enum([
|
||||
'STATE_CHANGED',
|
||||
'LOCATION_CHANGED',
|
||||
'FIELD_UPDATED',
|
||||
'FM_OPENED',
|
||||
'FM_CLOSED',
|
||||
'PART_SWAPPED',
|
||||
'TAG_ADDED',
|
||||
'TAG_REMOVED',
|
||||
@@ -41,9 +39,6 @@ export const HostEventType = z.enum([
|
||||
]);
|
||||
export type HostEventType = z.infer<typeof HostEventType>;
|
||||
|
||||
export const FmStatus = z.enum(['OPEN', 'CLOSED']);
|
||||
export type FmStatus = z.infer<typeof FmStatus>;
|
||||
|
||||
export const CsvImportStatus = z.enum([
|
||||
'PENDING',
|
||||
'STAGED',
|
||||
@@ -60,8 +55,6 @@ export const WebhookEventName = z.enum([
|
||||
'part.deleted',
|
||||
'part.state_changed',
|
||||
'part.location_changed',
|
||||
'fm.opened',
|
||||
'fm.closed',
|
||||
'repair.logged',
|
||||
'tag.assigned',
|
||||
'tag.removed',
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { FmStatus } from './enums.js';
|
||||
import { PaginationQuery } from './pagination.js';
|
||||
|
||||
// Host lookup accepts either a uuid `hostId` or a string `assetId` — exactly one.
|
||||
const hostSelector = {
|
||||
hostId: z.string().uuid().optional(),
|
||||
assetId: z.string().trim().min(1).max(128).optional(),
|
||||
};
|
||||
|
||||
function hostSelectorRefine<T extends { hostId?: string; assetId?: string }>(
|
||||
v: T,
|
||||
ctx: z.RefinementCtx,
|
||||
) {
|
||||
const has = [v.hostId, v.assetId].filter((x) => x !== undefined && x !== '').length;
|
||||
if (has !== 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Provide exactly one of hostId or assetId',
|
||||
path: ['hostId'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const CreateFmRequest = z
|
||||
.object({
|
||||
...hostSelector,
|
||||
problem: z.string().trim().min(1, 'Problem is required').max(2000),
|
||||
problemPartIds: z.array(z.string().uuid()).max(100).optional(),
|
||||
})
|
||||
.superRefine(hostSelectorRefine);
|
||||
export type CreateFmRequest = z.infer<typeof CreateFmRequest>;
|
||||
|
||||
export const UpdateFmRequest = z
|
||||
.object({
|
||||
status: FmStatus.optional(),
|
||||
problem: z.string().trim().min(1).max(2000).optional(),
|
||||
problemPartIds: z.array(z.string().uuid()).max(100).optional(),
|
||||
})
|
||||
.refine((v) => Object.keys(v).length > 0, { message: 'At least one field required' });
|
||||
export type UpdateFmRequest = z.infer<typeof UpdateFmRequest>;
|
||||
|
||||
export const FmListQuery = PaginationQuery.extend({
|
||||
status: FmStatus.optional(),
|
||||
hostId: z.string().uuid().optional(),
|
||||
problemPartId: z.string().uuid().optional(),
|
||||
openOnly: z
|
||||
.union([z.literal('true'), z.literal('false'), z.boolean()])
|
||||
.transform((v) => v === true || v === 'true')
|
||||
.optional(),
|
||||
});
|
||||
export type FmListQuery = z.infer<typeof FmListQuery>;
|
||||
@@ -9,7 +9,6 @@ export * from './env.js';
|
||||
export * from './pagination.js';
|
||||
export * from './hosts.js';
|
||||
export * from './host-events.js';
|
||||
export * from './fms.js';
|
||||
export * from './repairs.js';
|
||||
export * from './custody.js';
|
||||
export * from './tags.js';
|
||||
|
||||
@@ -14,7 +14,6 @@ export const LogRepairRequest = z
|
||||
brokenMpn: z.string().trim().min(1).max(128).optional(),
|
||||
brokenManufacturerId: z.string().uuid().optional(),
|
||||
replacementSerial: z.string().trim().min(1).max(128),
|
||||
fmId: z.string().uuid().optional(),
|
||||
})
|
||||
.superRefine((v, ctx) => {
|
||||
const hostHas = [v.hostId, v.assetId].filter((x) => x !== undefined && x !== '').length;
|
||||
@@ -33,7 +32,6 @@ export type LogRepairRequest = z.infer<typeof LogRepairRequest>;
|
||||
export const RepairListQuery = PaginationQuery.extend({
|
||||
hostId: z.string().uuid().optional(),
|
||||
performedById: z.string().uuid().optional(),
|
||||
fmId: z.string().uuid().optional(),
|
||||
since: z.string().datetime().optional(),
|
||||
});
|
||||
export type RepairListQuery = z.infer<typeof RepairListQuery>;
|
||||
|
||||
Reference in New Issue
Block a user