From fd39fff433b583cad853ae5a73e72c4d2ad3e20a Mon Sep 17 00:00:00 2001 From: otto-bot Date: Wed, 6 May 2026 13:56:57 -0400 Subject: [PATCH] CUB-111: merge PostgreSQL schema and Go models into dev --- backend/internal/models/models.go | 162 ++++++++++++ .../migrations/000001_initial_schema.down.sql | 19 ++ .../migrations/000001_initial_schema.up.sql | 231 ++++++++++++++++++ backend/migrations/000002_seed_data.down.sql | 15 ++ backend/migrations/000002_seed_data.up.sql | 95 +++++++ 5 files changed, 522 insertions(+) create mode 100644 backend/internal/models/models.go create mode 100644 backend/migrations/000001_initial_schema.down.sql create mode 100644 backend/migrations/000001_initial_schema.up.sql create mode 100644 backend/migrations/000002_seed_data.down.sql create mode 100644 backend/migrations/000002_seed_data.up.sql diff --git a/backend/internal/models/models.go b/backend/internal/models/models.go new file mode 100644 index 0000000..19338ac --- /dev/null +++ b/backend/internal/models/models.go @@ -0,0 +1,162 @@ +// Package models defines the Extrudex domain model structs. +// These map 1:1 to PostgreSQL tables with snake_case JSON serialization. +// Nullable fields use pointer types; all timestamps are time.Time. +package models + +import "time" + +// ============================================================================ +// Lookup Tables +// ============================================================================ + +// PrinterType represents a printer technology category (fdm, resin, etc.). +type PrinterType struct { + ID int `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// JobStatus represents a print job lifecycle state. +type JobStatus struct { + ID int `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// MaterialBase represents a base material type (PLA, PETG, ABS, etc.). +// Density and temperature ranges are stored here for grams-calculation and slicing guidance. +type MaterialBase struct { + ID int `json:"id"` + Name string `json:"name"` + DensityGCm3 float64 `json:"density_g_cm3"` + ExtrusionTempMin *int `json:"extrusion_temp_min,omitempty"` + ExtrusionTempMax *int `json:"extrusion_temp_max,omitempty"` + BedTempMin *int `json:"bed_temp_min,omitempty"` + BedTempMax *int `json:"bed_temp_max,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// MaterialFinish represents the visual/texture finish (Basic, Silk, Matte, etc.). +type MaterialFinish struct { + ID int `json:"id"` + Name string `json:"name"` + Description *string `json:"description,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// MaterialModifier represents an additive property (Carbon Fiber, Wood-Filled, etc.). +type MaterialModifier struct { + ID int `json:"id"` + Name string `json:"name"` + Description *string `json:"description,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ============================================================================ +// Core Entity Tables +// ============================================================================ + +// Printer represents a 3D printer in the fleet. +type Printer struct { + ID int `json:"id"` + Name string `json:"name"` + PrinterTypeID int `json:"printer_type_id"` + PrinterType *PrinterType `json:"printer_type,omitempty"` // populated on JOIN queries + Manufacturer *string `json:"manufacturer,omitempty"` + Model *string `json:"model,omitempty"` + MoonrakerURL *string `json:"moonraker_url,omitempty"` + MoonrakerAPIKey *string `json:"moonraker_api_key,omitempty"` + MQTTBrokerHost *string `json:"mqtt_broker_host,omitempty"` + MQTTTopicPrefix *string `json:"mqtt_topic_prefix,omitempty"` + MQTTTLSEnabled bool `json:"mqtt_tls_enabled"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// FilamentSpool represents a physical filament spool in inventory. +// material_finish_id defaults to 1 ("Basic"); material_modifier_id is optional. +// Grams are always physically measured values — grams_used is derived, not stored. +type FilamentSpool struct { + ID int `json:"id"` + Name string `json:"name"` + MaterialBaseID int `json:"material_base_id"` + MaterialBase *MaterialBase `json:"material_base,omitempty"` // JOIN + MaterialFinishID int `json:"material_finish_id"` + MaterialFinish *MaterialFinish `json:"material_finish,omitempty"` // JOIN + MaterialModifierID *int `json:"material_modifier_id,omitempty"` + MaterialModifier *MaterialModifier `json:"material_modifier,omitempty"` // JOIN + ColorHex string `json:"color_hex"` + Brand *string `json:"brand,omitempty"` + DiameterMM float64 `json:"diameter_mm"` + InitialGrams int `json:"initial_grams"` + RemainingGrams int `json:"remaining_grams"` + SpoolWeightGrams *int `json:"spool_weight_grams,omitempty"` + CostUSD *float64 `json:"cost_usd,omitempty"` + LowStockThresholdGrams int `json:"low_stock_threshold_grams"` + Notes *string `json:"notes,omitempty"` + Barcode *string `json:"barcode,omitempty"` + DeletedAt *time.Time `json:"deleted_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// PrintJob represents a single print on a specific printer. +// The filament_spool_id is a convenience reference; multi-spool jobs track usage in usage_logs. +type PrintJob struct { + ID int `json:"id"` + PrinterID int `json:"printer_id"` + Printer *Printer `json:"printer,omitempty"` // JOIN + FilamentSpoolID *int `json:"filament_spool_id,omitempty"` + FilamentSpool *FilamentSpool `json:"filament_spool,omitempty"` // JOIN + JobName string `json:"job_name"` + FileName *string `json:"file_name,omitempty"` + JobStatusID int `json:"job_status_id"` + JobStatus *JobStatus `json:"job_status,omitempty"` // JOIN + StartedAt *time.Time `json:"started_at,omitempty"` + CompletedAt *time.Time `json:"completed_at,omitempty"` + DurationSeconds *int `json:"duration_seconds,omitempty"` + EstimatedDurationSeconds *int `json:"estimated_duration_seconds,omitempty"` + TotalMMExtruded *float64 `json:"total_mm_extruded,omitempty"` + TotalGramsUsed *float64 `json:"total_grams_used,omitempty"` + TotalCostUSD *float64 `json:"total_cost_usd,omitempty"` + Notes *string `json:"notes,omitempty"` + DeletedAt *time.Time `json:"deleted_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// UsageLog records filament consumption for a specific spool during a print job. +// This is the atomic unit of filament tracking — grams are derived from mm_extruded. +type UsageLog struct { + ID int `json:"id"` + PrintJobID int `json:"print_job_id"` + PrintJob *PrintJob `json:"print_job,omitempty"` // JOIN + FilamentSpoolID int `json:"filament_spool_id"` + FilamentSpool *FilamentSpool `json:"filament_spool,omitempty"` // JOIN + MMExtruded float64 `json:"mm_extruded"` + GramsUsed float64 `json:"grams_used"` + CostUSD *float64 `json:"cost_usd,omitempty"` + LoggedAt time.Time `json:"logged_at"` + CreatedAt time.Time `json:"created_at"` +} + +// ============================================================================ +// Application Settings +// ============================================================================ + +// Setting represents a key-value application configuration entry. +// The value is stored as JSONB in PostgreSQL, allowing flexible typed config. +type Setting struct { + ID int `json:"id"` + Key string `json:"key"` + Value []byte `json:"value"` // raw JSON — marshalled/unmarshalled by caller + Description *string `json:"description,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/backend/migrations/000001_initial_schema.down.sql b/backend/migrations/000001_initial_schema.down.sql new file mode 100644 index 0000000..a7e0f84 --- /dev/null +++ b/backend/migrations/000001_initial_schema.down.sql @@ -0,0 +1,19 @@ +-- Migration: 000001_initial_schema (rollback) +-- Description: Drop all tables and indexes created in the initial schema migration +-- Author: Hex +-- Date: 2026-05-06 + +BEGIN; + +DROP TABLE IF EXISTS usage_logs CASCADE; +DROP TABLE IF EXISTS print_jobs CASCADE; +DROP TABLE IF EXISTS filament_spools CASCADE; +DROP TABLE IF EXISTS printers CASCADE; +DROP TABLE IF EXISTS settings CASCADE; +DROP TABLE IF EXISTS material_modifiers CASCADE; +DROP TABLE IF EXISTS material_finishes CASCADE; +DROP TABLE IF EXISTS material_bases CASCADE; +DROP TABLE IF EXISTS job_statuses CASCADE; +DROP TABLE IF EXISTS printer_types CASCADE; + +COMMIT; diff --git a/backend/migrations/000001_initial_schema.up.sql b/backend/migrations/000001_initial_schema.up.sql new file mode 100644 index 0000000..db086fc --- /dev/null +++ b/backend/migrations/000001_initial_schema.up.sql @@ -0,0 +1,231 @@ +-- Migration: 000001_initial_schema +-- Description: Create initial Extrudex schema — lookup tables, core entities, and settings +-- Author: Hex +-- Date: 2026-05-06 +-- +-- Design decisions: +-- - Lookup tables for material_base, material_finish, material_modifier (no free-text enums) +-- - Lookup tables for printer_type and job_status (extensible, no hard-coded enum values) +-- - FK ON DELETE: RESTRICT on critical parents (material_base, material_finish, printer), +-- SET NULL on optional parents (modifier, spool on print_jobs), +-- CASCADE for usage_logs when parent job is deleted +-- - Soft-delete (deleted_at) on spools and print_jobs for safety +-- - JSONB config column on settings for flexible app-wide configuration +-- - All identifiers snake_case per project convention + +BEGIN; + +-- ============================================================================ +-- Lookup Tables +-- ============================================================================ + +-- Printer types (fdm, resin, etc.) — extensible, not a raw enum +CREATE TABLE printer_types ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Job statuses (pending, printing, paused, completed, failed, cancelled) +CREATE TABLE job_statuses ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Material base types (PLA, PETG, ABS, TPU, ASA, Nylon, PC) +CREATE TABLE material_bases ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + density_g_cm3 DECIMAL(5,3) NOT NULL, + extrusion_temp_min INT, + extrusion_temp_max INT, + bed_temp_min INT, + bed_temp_max INT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Material finishes (Basic, Silk, Matte, Glossy, Satin) +CREATE TABLE material_finishes ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Material modifiers (Wood-Filled, Carbon Fiber, Glow-in-Dark, Marble) +CREATE TABLE material_modifiers ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ============================================================================ +-- Core Entity Tables +-- ============================================================================ + +-- 3D printers in the fleet +CREATE TABLE printers ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + printer_type_id INT NOT NULL, + manufacturer VARCHAR(255), + model VARCHAR(255), + moonraker_url VARCHAR(512), + moonraker_api_key VARCHAR(512), + mqtt_broker_host VARCHAR(255), + mqtt_topic_prefix VARCHAR(255), + mqtt_tls_enabled BOOLEAN NOT NULL DEFAULT FALSE, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT fk_printers_printer_type + FOREIGN KEY (printer_type_id) REFERENCES printer_types(id) + ON DELETE RESTRICT +); + +-- Filament spools — the core inventory item +CREATE TABLE filament_spools ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + material_base_id INT NOT NULL, + material_finish_id INT NOT NULL DEFAULT 1, -- "Basic" (seed data populates this first) + material_modifier_id INT, + color_hex VARCHAR(7) NOT NULL CHECK (color_hex ~ '^#[0-9A-Fa-f]{6}$'), + brand VARCHAR(255), + diameter_mm DECIMAL(4,2) NOT NULL DEFAULT 1.75, + initial_grams INT NOT NULL CHECK (initial_grams > 0), + remaining_grams INT NOT NULL CHECK (remaining_grams >= 0), + spool_weight_grams INT, -- measured empty-spool weight (tare), nullable + cost_usd DECIMAL(10,2), + low_stock_threshold_grams INT NOT NULL DEFAULT 50, + notes TEXT, + barcode VARCHAR(255) UNIQUE, + deleted_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT fk_spools_material_base + FOREIGN KEY (material_base_id) REFERENCES material_bases(id) + ON DELETE RESTRICT, + + CONSTRAINT fk_spools_material_finish + FOREIGN KEY (material_finish_id) REFERENCES material_finishes(id) + ON DELETE RESTRICT, + + CONSTRAINT fk_spools_material_modifier + FOREIGN KEY (material_modifier_id) REFERENCES material_modifiers(id) + ON DELETE SET NULL +); + +-- Print jobs — each job is one print on one printer +CREATE TABLE print_jobs ( + id SERIAL PRIMARY KEY, + printer_id INT NOT NULL, + filament_spool_id INT, -- nullable: a job may use multiple spools (captured in usage_logs) + job_name VARCHAR(255) NOT NULL, + file_name VARCHAR(512), + job_status_id INT NOT NULL DEFAULT 1, -- "pending" + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + duration_seconds INT, + estimated_duration_seconds INT, + total_mm_extruded DECIMAL(12,2), + total_grams_used DECIMAL(10,2), + total_cost_usd DECIMAL(10,4), + notes TEXT, + deleted_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT fk_print_jobs_printer + FOREIGN KEY (printer_id) REFERENCES printers(id) + ON DELETE RESTRICT, + + CONSTRAINT fk_print_jobs_spool + FOREIGN KEY (filament_spool_id) REFERENCES filament_spools(id) + ON DELETE SET NULL, + + CONSTRAINT fk_print_jobs_status + FOREIGN KEY (job_status_id) REFERENCES job_statuses(id) + ON DELETE RESTRICT +); + +-- Usage logs — granular tracking of filament consumed per job, per spool +CREATE TABLE usage_logs ( + id SERIAL PRIMARY KEY, + print_job_id INT NOT NULL, + filament_spool_id INT NOT NULL, + mm_extruded DECIMAL(12,2) NOT NULL CHECK (mm_extruded > 0), + grams_used DECIMAL(10,2) NOT NULL CHECK (grams_used > 0), + cost_usd DECIMAL(10,4), + logged_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT fk_usage_logs_print_job + FOREIGN KEY (print_job_id) REFERENCES print_jobs(id) + ON DELETE CASCADE, + + CONSTRAINT fk_usage_logs_spool + FOREIGN KEY (filament_spool_id) REFERENCES filament_spools(id) + ON DELETE RESTRICT +); + +-- ============================================================================ +-- Application Settings +-- ============================================================================ + +CREATE TABLE settings ( + id SERIAL PRIMARY KEY, + key VARCHAR(255) NOT NULL UNIQUE, + value JSONB NOT NULL, + description TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ============================================================================ +-- Indexes +-- ============================================================================ + +-- Filament spools — query patterns: lookup by material, low-stock scans, barcode scans +CREATE INDEX ix_spools_material_base_id ON filament_spools(material_base_id); +CREATE INDEX ix_spools_material_finish_id ON filament_spools(material_finish_id); +CREATE INDEX ix_spools_material_modifier_id ON filament_spools(material_modifier_id); +CREATE INDEX ix_spools_remaining_grams ON filament_spools(remaining_grams) + WHERE deleted_at IS NULL; -- partial index: only active spools for low-stock queries +CREATE INDEX ix_spools_barcode ON filament_spools(barcode) + WHERE barcode IS NOT NULL AND deleted_at IS NULL; +CREATE INDEX ix_spools_deleted_at ON filament_spools(deleted_at) + WHERE deleted_at IS NOT NULL; -- small index for soft-delete filtering + +-- Printers +CREATE INDEX ix_printers_printer_type_id ON printers(printer_type_id); +CREATE INDEX ix_printers_is_active ON printers(is_active) + WHERE is_active = TRUE; -- partial index for fleet dashboard queries + +-- Print jobs — query by printer, status, date range, and soft-delete filter +CREATE INDEX ix_print_jobs_printer_id ON print_jobs(printer_id); +CREATE INDEX ix_print_jobs_spool_id ON print_jobs(filament_spool_id) + WHERE filament_spool_id IS NOT NULL; +CREATE INDEX ix_print_jobs_status_id ON print_jobs(job_status_id); +CREATE INDEX ix_print_jobs_created_at ON print_jobs(created_at DESC); +CREATE INDEX ix_print_jobs_deleted_at ON print_jobs(deleted_at) + WHERE deleted_at IS NOT NULL; + +-- Usage logs — always queried by job or spool +CREATE INDEX ix_usage_logs_print_job_id ON usage_logs(print_job_id); +CREATE INDEX ix_usage_logs_spool_id ON usage_logs(filament_spool_id); +CREATE INDEX ix_usage_logs_logged_at ON usage_logs(logged_at DESC); + +-- Settings — key lookups +CREATE INDEX ix_settings_key ON settings(key); + +COMMIT; diff --git a/backend/migrations/000002_seed_data.down.sql b/backend/migrations/000002_seed_data.down.sql new file mode 100644 index 0000000..78dd8bf --- /dev/null +++ b/backend/migrations/000002_seed_data.down.sql @@ -0,0 +1,15 @@ +-- Migration: 000002_seed_data (rollback) +-- Description: Remove seed data inserted in 000002 +-- Author: Hex +-- Date: 2026-05-06 + +BEGIN; + +DELETE FROM settings WHERE key IN ('default_low_stock_threshold_grams', 'default_diameter_mm', 'filament_cross_section_area_mm2'); +DELETE FROM material_modifiers WHERE id IN (1, 2, 3, 4); +DELETE FROM material_finishes WHERE id IN (1, 2, 3, 4, 5); +DELETE FROM material_bases WHERE id IN (1, 2, 3, 4, 5, 6, 7); +DELETE FROM job_statuses WHERE id IN (1, 2, 3, 4, 5, 6); +DELETE FROM printer_types WHERE id IN (1, 2); + +COMMIT; diff --git a/backend/migrations/000002_seed_data.up.sql b/backend/migrations/000002_seed_data.up.sql new file mode 100644 index 0000000..6c573eb --- /dev/null +++ b/backend/migrations/000002_seed_data.up.sql @@ -0,0 +1,95 @@ +-- Seed Data: Extrudex common reference data +-- Author: Hex +-- Date: 2026-05-06 +-- +-- IMPORTANT: IDs are explicitly assigned to satisfy the DEFAULT constraints: +-- - filament_spools.material_finish_id DEFAULT 1 ("Basic") +-- - print_jobs.job_status_id DEFAULT 1 ("pending") +-- +-- Density values sourced from common manufacturer specifications. +-- Temperature ranges are conservative/typical; users can override per-spool. + +BEGIN; + +-- ============================================================================ +-- Printer Types +-- ============================================================================ + +INSERT INTO printer_types (id, name) VALUES + (1, 'fdm'), + (2, 'resin') +ON CONFLICT (id) DO NOTHING; + +-- Reset the sequence so future inserts start after our explicit IDs +SELECT setval('printer_types_id_seq', GREATEST(2, (SELECT MAX(id) FROM printer_types))); + +-- ============================================================================ +-- Job Statuses +-- ============================================================================ + +INSERT INTO job_statuses (id, name) VALUES + (1, 'pending'), + (2, 'printing'), + (3, 'paused'), + (4, 'completed'), + (5, 'failed'), + (6, 'cancelled') +ON CONFLICT (id) DO NOTHING; + +SELECT setval('job_statuses_id_seq', GREATEST(6, (SELECT MAX(id) FROM job_statuses))); + +-- ============================================================================ +-- Material Bases (common filament types) +-- ============================================================================ + +INSERT INTO material_bases (id, name, density_g_cm3, extrusion_temp_min, extrusion_temp_max, bed_temp_min, bed_temp_max) VALUES + (1, 'PLA', 1.24, 190, 220, 0, 60), + (2, 'PETG', 1.27, 230, 250, 70, 90), + (3, 'ABS', 1.04, 230, 260, 90, 110), + (4, 'TPU', 1.21, 220, 250, 0, 60), + (5, 'ASA', 1.07, 240, 260, 90, 110), + (6, 'Nylon', 1.14, 240, 280, 70, 100), + (7, 'PC', 1.20, 260, 310, 90, 120) +ON CONFLICT (id) DO NOTHING; + +SELECT setval('material_bases_id_seq', GREATEST(7, (SELECT MAX(id) FROM material_bases))); + +-- ============================================================================ +-- Material Finishes +-- ============================================================================ +-- ID 1 = "Basic" is the default for new spools (DEFAULT 1 constraint) + +INSERT INTO material_finishes (id, name, description) VALUES + (1, 'Basic', 'Standard solid-color filament with no special finish'), + (2, 'Silk', 'Glossy silk-like sheen, often used for decorative prints'), + (3, 'Matte', 'Flat non-reflective surface finish'), + (4, 'Glossy', 'High-shine reflective surface'), + (5, 'Satin', 'Semi-gloss between matte and glossy') +ON CONFLICT (id) DO NOTHING; + +SELECT setval('material_finishes_id_seq', GREATEST(5, (SELECT MAX(id) FROM material_finishes))); + +-- ============================================================================ +-- Material Modifiers +-- ============================================================================ + +INSERT INTO material_modifiers (id, name, description) VALUES + (1, 'Wood-Filled', 'Contains wood fibers for natural wood-like appearance and texture'), + (2, 'Carbon Fiber', 'Reinforced with carbon fibers for increased stiffness and strength'), + (3, 'Glow-in-Dark', 'Phosphorescent additive that glows after exposure to light'), + (4, 'Marble', 'Contains specks for a stone-like marble appearance') +ON CONFLICT (id) DO NOTHING; + +SELECT setval('material_modifiers_id_seq', GREATEST(4, (SELECT MAX(id) FROM material_modifiers))); + +-- ============================================================================ +-- Default Application Settings +-- ============================================================================ + +INSERT INTO settings (key, value, description) VALUES + ('default_low_stock_threshold_grams', '50', 'Default grams threshold for low-stock alerts on new spools'), + ('default_diameter_mm', '1.75', 'Default filament diameter for new spools (1.75mm is the modern standard)'), + ('filament_cross_section_area_mm2', '2.405', 'Cross-sectional area for 1.75mm filament: π × (1.75/2)²') +ON CONFLICT (key) DO NOTHING; + +COMMIT;