All checks were successful
Deploy production / deploy (push) Successful in 1m4s
- model the existing hardware schema in Drizzle (devices, components, new sensitivities) - idempotent reconciling migration: new columns, checks, setup_overview view, grants, meta.tables - seed: main PC labels/hostnames, planned AMD X3D + RTX 5090 build, stan-ovh (OVH RISE-S), other devices, sensitivity placeholders (no invented cm/360) - owner-only add/edit via modals; public view hides notes/order numbers - trackers dropdown entry + icon, seo/title/sitemap, owner write prefix
192 lines
6.9 KiB
TypeScript
192 lines
6.9 KiB
TypeScript
import { sql } from 'drizzle-orm';
|
||
import {
|
||
bigint,
|
||
boolean,
|
||
check,
|
||
date,
|
||
index,
|
||
integer,
|
||
jsonb,
|
||
numeric,
|
||
text,
|
||
unique
|
||
} from 'drizzle-orm/pg-core';
|
||
import { hardwareSchema } from './schemas';
|
||
import { timestamptz } from './helpers';
|
||
|
||
// PC setup tracker (/trackers/pc): every device Stan owns or plans (PCs, laptops, servers,
|
||
// phones, tablets) and the parts/peripherals on them, plus game sensitivities (cm/360) so
|
||
// every mouse feels the same everywhere. The tables predate Drizzle (hand-made in Oct 2026
|
||
// from Stan's own list + Megekko/Alternate order mails), so ids are bigint identities.
|
||
// Unknown stays null; nothing is estimated. Order/invoice numbers and notes are owner-only.
|
||
|
||
export const HARDWARE_DEVICE_KINDS = [
|
||
'desktop',
|
||
'laptop',
|
||
'server',
|
||
'vm',
|
||
'phone',
|
||
'tablet',
|
||
'monitor',
|
||
'other'
|
||
] as const;
|
||
export const HARDWARE_DEVICE_STATUSES = ['active', 'spare', 'planned', 'retired', 'sold'] as const;
|
||
export const HARDWARE_CATEGORIES = [
|
||
'case',
|
||
'cpu',
|
||
'motherboard',
|
||
'ram',
|
||
'cpu_cooler',
|
||
'gpu',
|
||
'psu',
|
||
'storage',
|
||
'fan',
|
||
'lighting',
|
||
'monitor',
|
||
'mouse',
|
||
'keyboard',
|
||
'headset',
|
||
'audio',
|
||
'controller',
|
||
'accessory',
|
||
'other'
|
||
] as const;
|
||
export const HARDWARE_STATUSES = ['in_use', 'planned', 'spare', 'retired', 'sold'] as const;
|
||
|
||
const list = (values: readonly string[]) => sql.raw(values.map((v) => `'${v}'`).join(', '));
|
||
|
||
/** Structured specs; free-form per category, e.g. {"socket":"AM5","cores":8}. */
|
||
export type HardwareSpecs = Record<string, string | number | boolean | null>;
|
||
|
||
export const hardwareDevices = hardwareSchema.table(
|
||
'devices',
|
||
{
|
||
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
||
/** Unique hostname-style slug, e.g. stan-pc, stan-ovh. */
|
||
name: text('name').notNull().unique(),
|
||
/** Display name, e.g. "Main PC". */
|
||
label: text('label'),
|
||
/** Other hostnames/OSes this device runs as, e.g. {stan-arch, stan-win11}. */
|
||
hostnames: text('hostnames').array().notNull().default([]),
|
||
kind: text('kind').notNull(),
|
||
status: text('status').notNull().default('active'),
|
||
brand: text('brand'),
|
||
model: text('model'),
|
||
specs: jsonb('specs').$type<HardwareSpecs>().notNull().default({}),
|
||
/** One-off purchase price, euro cents incl. VAT. */
|
||
priceCents: integer('price_cents'),
|
||
/** Recurring cost per month, euro cents (servers/VMs); see notes for VAT. */
|
||
monthlyCents: integer('monthly_cents'),
|
||
/** Budget for planned builds, euro cents. */
|
||
budgetCents: integer('budget_cents'),
|
||
shop: text('shop'),
|
||
purchaseDate: date('purchase_date'),
|
||
url: text('url'),
|
||
imageUrl: text('image_url'),
|
||
sortOrder: integer('sort_order').notNull().default(100),
|
||
notes: text('notes'),
|
||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||
updatedAt: timestamptz('updated_at')
|
||
.notNull()
|
||
.defaultNow()
|
||
.$onUpdate(() => new Date())
|
||
},
|
||
() => [
|
||
check('devices_kind_check', sql`kind in (${list(HARDWARE_DEVICE_KINDS)})`),
|
||
check('devices_status_check', sql`status in (${list(HARDWARE_DEVICE_STATUSES)})`),
|
||
check(
|
||
'devices_cents_check',
|
||
sql`(price_cents is null or price_cents >= 0) and (monthly_cents is null or monthly_cents >= 0) and (budget_cents is null or budget_cents >= 0)`
|
||
)
|
||
]
|
||
);
|
||
|
||
export const hardwareComponents = hardwareSchema.table(
|
||
'components',
|
||
{
|
||
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
||
deviceId: bigint('device_id', { mode: 'number' })
|
||
.notNull()
|
||
.references(() => hardwareDevices.id, { onDelete: 'cascade' }),
|
||
category: text('category').notNull(),
|
||
brand: text('brand'),
|
||
model: text('model').notNull(),
|
||
quantity: integer('quantity').notNull().default(1),
|
||
specs: jsonb('specs').$type<HardwareSpecs>().notNull().default({}),
|
||
/** Per unit, euro cents incl. VAT. */
|
||
unitPriceCents: integer('unit_price_cents'),
|
||
/** unit price × quantity, euro cents incl. VAT, excl. shipping. */
|
||
totalPriceCents: integer('total_price_cents'),
|
||
shop: text('shop'),
|
||
orderNumber: text('order_number'),
|
||
orderDate: date('order_date'),
|
||
warrantyUntil: date('warranty_until'),
|
||
invoiceNumber: text('invoice_number'),
|
||
status: text('status').notNull().default('in_use'),
|
||
/** false: the purchase is real but whether it is (still) in this setup is unconfirmed. */
|
||
verified: boolean('verified').notNull().default(true),
|
||
/** Mice: DPI set in the mouse software, and the real DPI measured with a ruler. */
|
||
dpi: integer('dpi'),
|
||
measuredDpi: integer('measured_dpi'),
|
||
url: text('url'),
|
||
imageUrl: text('image_url'),
|
||
notes: text('notes'),
|
||
/** user_list | gmail | user_statement | manual | plan. */
|
||
source: text('source'),
|
||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||
updatedAt: timestamptz('updated_at')
|
||
.notNull()
|
||
.defaultNow()
|
||
.$onUpdate(() => new Date())
|
||
},
|
||
(t) => [
|
||
check('components_category_check', sql`category in (${list(HARDWARE_CATEGORIES)})`),
|
||
check('components_status_check', sql`status in (${list(HARDWARE_STATUSES)})`),
|
||
check('components_quantity_check', sql`quantity > 0`),
|
||
check('components_unit_price_cents_check', sql`unit_price_cents >= 0`),
|
||
check('components_total_price_cents_check', sql`total_price_cents >= 0`),
|
||
check(
|
||
'components_dpi_check',
|
||
sql`(dpi is null or dpi > 0) and (measured_dpi is null or measured_dpi > 0)`
|
||
),
|
||
unique('components_dedupe_uq').on(t.deviceId, t.model, t.orderNumber).nullsNotDistinct(),
|
||
index('components_device_idx').on(t.deviceId),
|
||
index('components_category_idx').on(t.category),
|
||
index('components_specs_gin').using('gin', t.specs)
|
||
]
|
||
);
|
||
|
||
/**
|
||
* One row per game: the physical sensitivity Stan wants (cm per 360° turn) and, where the
|
||
* game's yaw is known (degrees per mouse count at sens 1, e.g. CS2 0.022), the in-game value
|
||
* is computed per mouse from its measured DPI. Stored values are only what Stan entered.
|
||
*/
|
||
export const hardwareSensitivities = hardwareSchema.table(
|
||
'sensitivities',
|
||
{
|
||
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
||
game: text('game').notNull().unique(),
|
||
cmPer360: numeric('cm_per_360', { precision: 7, scale: 2, mode: 'number' }),
|
||
/** Degrees per count at sensitivity 1 (CS2/Source: 0.022). */
|
||
yaw: numeric('yaw', { precision: 10, scale: 6, mode: 'number' }),
|
||
/** What is set in-game right now (as entered by Stan). */
|
||
inGameSens: numeric('in_game_sens', { precision: 10, scale: 4, mode: 'number' }),
|
||
/** Mouse this value was set with (a hardware.components row, category mouse). */
|
||
mouseId: bigint('mouse_id', { mode: 'number' }).references(() => hardwareComponents.id, {
|
||
onDelete: 'set null'
|
||
}),
|
||
fov: text('fov'),
|
||
notes: text('notes'),
|
||
sortOrder: integer('sort_order').notNull().default(100),
|
||
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||
updatedAt: timestamptz('updated_at')
|
||
.notNull()
|
||
.defaultNow()
|
||
.$onUpdate(() => new Date())
|
||
},
|
||
() => [
|
||
check('sensitivities_cm_check', sql`cm_per_360 is null or cm_per_360 > 0`),
|
||
check('sensitivities_yaw_check', sql`yaw is null or yaw > 0`)
|
||
]
|
||
);
|