b0e9c5d1d0
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>
325 lines
9.7 KiB
Plaintext
325 lines
9.7 KiB
Plaintext
// NOTE: provider is temporarily set to "sqlite" for Phase 1 local verification.
|
|
// Flip to "postgresql" once Docker + docker-compose Postgres is available.
|
|
// All cascade rules and indexes below are portable between providers.
|
|
//
|
|
// Postgres-only additions applied post-cutover (see packages/db/POSTGRES_FTS.md):
|
|
// * Generated tsvector column on Part(serial, mpn, notes) + GIN index (Phase 3 scope).
|
|
// * WebhookSubscription.events and SavedView.filterJson currently stored as String (JSON
|
|
// text) for SQLite portability; on Postgres these become String[] and Jsonb respectively.
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "sqlite"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(uuid())
|
|
username String @unique
|
|
email String @unique
|
|
passwordHash String
|
|
role String @default("TECHNICIAN")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
partEvents PartEvent[]
|
|
hostEvents HostEvent[]
|
|
refreshTokens RefreshToken[]
|
|
custodyParts Part[] @relation("Custody")
|
|
repairs Repair[]
|
|
savedViews SavedView[]
|
|
csvImportJobs CsvImportJob[]
|
|
}
|
|
|
|
model RefreshToken {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
tokenHash String @unique
|
|
expiresAt DateTime
|
|
revokedAt DateTime?
|
|
replacedBy String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([expiresAt])
|
|
}
|
|
|
|
model Manufacturer {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
parts Part[]
|
|
partModels PartModel[]
|
|
}
|
|
|
|
model PartModel {
|
|
id String @id @default(uuid())
|
|
manufacturerId String
|
|
manufacturer Manufacturer @relation(fields: [manufacturerId], references: [id], onDelete: Restrict)
|
|
mpn String
|
|
eolDate DateTime?
|
|
destroyOnFail Boolean @default(false)
|
|
notes String?
|
|
categoryId String?
|
|
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
parts Part[]
|
|
|
|
@@unique([manufacturerId, mpn])
|
|
@@index([manufacturerId])
|
|
@@index([eolDate])
|
|
@@index([categoryId])
|
|
}
|
|
|
|
model Site {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
rooms Room[]
|
|
}
|
|
|
|
model Room {
|
|
id String @id @default(uuid())
|
|
name String
|
|
siteId String
|
|
site Site @relation(fields: [siteId], references: [id], onDelete: Cascade)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
bins Bin[]
|
|
|
|
@@unique([siteId, name])
|
|
@@index([siteId])
|
|
}
|
|
|
|
model Bin {
|
|
id String @id @default(uuid())
|
|
name String
|
|
roomId String
|
|
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
parts Part[]
|
|
|
|
@@unique([roomId, name])
|
|
@@index([roomId])
|
|
}
|
|
|
|
model Category {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
description String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
partModels PartModel[]
|
|
}
|
|
|
|
model Part {
|
|
id String @id @default(uuid())
|
|
serialNumber String @unique
|
|
partModelId String
|
|
partModel PartModel @relation(fields: [partModelId], references: [id], onDelete: Restrict)
|
|
manufacturerId String
|
|
manufacturer Manufacturer @relation(fields: [manufacturerId], references: [id], onDelete: Restrict)
|
|
price Float?
|
|
state String @default("SPARE")
|
|
binId String?
|
|
bin Bin? @relation(fields: [binId], references: [id], onDelete: SetNull)
|
|
hostId String?
|
|
host Host? @relation(fields: [hostId], references: [id], onDelete: SetNull)
|
|
custodianId String?
|
|
custodian User? @relation("Custody", fields: [custodianId], references: [id], onDelete: SetNull)
|
|
notes String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
events PartEvent[]
|
|
tags PartTag[]
|
|
problemInFms FmPart[]
|
|
brokenRepairs Repair[] @relation("BrokenRepairs")
|
|
replacementRepairs Repair[] @relation("ReplacementRepairs")
|
|
|
|
@@index([state])
|
|
@@index([binId])
|
|
@@index([manufacturerId])
|
|
@@index([partModelId])
|
|
@@index([hostId])
|
|
@@index([custodianId])
|
|
}
|
|
|
|
model PartEvent {
|
|
id String @id @default(uuid())
|
|
partId String
|
|
part Part @relation(fields: [partId], references: [id], onDelete: Cascade)
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
type String
|
|
field String?
|
|
oldValue String?
|
|
newValue String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([partId, createdAt(sort: Desc)])
|
|
@@index([userId])
|
|
}
|
|
|
|
model Tag {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
color String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
parts PartTag[]
|
|
}
|
|
|
|
model PartTag {
|
|
partId String
|
|
tagId String
|
|
part Part @relation(fields: [partId], references: [id], onDelete: Cascade)
|
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([partId, tagId])
|
|
@@index([tagId])
|
|
}
|
|
|
|
model Host {
|
|
id String @id @default(uuid())
|
|
assetId String @unique
|
|
name String @unique
|
|
location String?
|
|
notes String?
|
|
state String @default("DEPLOYED")
|
|
stack String @default("PRODUCTION")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
parts Part[]
|
|
fms Fm[]
|
|
repairs Repair[]
|
|
events HostEvent[]
|
|
|
|
@@index([state])
|
|
@@index([stack])
|
|
}
|
|
|
|
model HostEvent {
|
|
id String @id @default(uuid())
|
|
hostId String
|
|
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
type String
|
|
field String?
|
|
oldValue String?
|
|
newValue String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([hostId, createdAt(sort: Desc)])
|
|
@@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
|
|
host Host @relation(fields: [hostId], references: [id], onDelete: Restrict)
|
|
brokenPartId String
|
|
brokenPart Part @relation("BrokenRepairs", fields: [brokenPartId], references: [id], onDelete: Restrict)
|
|
replacementPartId String
|
|
replacement Part @relation("ReplacementRepairs", fields: [replacementPartId], references: [id], onDelete: Restrict)
|
|
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])
|
|
@@map("repairs")
|
|
}
|
|
|
|
model WebhookSubscription {
|
|
id String @id @default(uuid())
|
|
url String
|
|
secret String
|
|
// JSON array of WebhookEventName values. Becomes String[] on Postgres cutover.
|
|
events String
|
|
active Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([active])
|
|
}
|
|
|
|
model SavedView {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
resource String
|
|
name String
|
|
// JSON blob describing filters/sort/columns. Becomes Jsonb on Postgres cutover.
|
|
filterJson String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([userId, resource, name])
|
|
@@index([userId])
|
|
}
|
|
|
|
model CsvImportJob {
|
|
id String @id @default(uuid())
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
resource String
|
|
status String @default("PENDING")
|
|
filename String
|
|
stagedRows Int @default(0)
|
|
// JSON array of CsvImportRowError. Nullable until validation runs.
|
|
errors String?
|
|
startedAt DateTime?
|
|
finishedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
@@index([status])
|
|
}
|