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; 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().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().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`) ] );