import pg from 'pg' const { Pool } = pg export const pool = new Pool({ connectionString: process.env.DATABASE_URL }) export async function initDb() { await pool.query(` -- App-wide key/value status (last fetch time/provider/error) — same KV pattern as -- tasks' config table, just used for status rather than user-facing settings since -- the real settings (hotel_location, met_office key) live centrally in the -- shared 'settings' app, not here. CREATE TABLE IF NOT EXISTS status ( key TEXT PRIMARY KEY, value JSONB NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- One row per forecast data point. 'resolution' is which display tier it belongs -- to: 'hourly' (today, full day), '3hourly' (next 2 days), 'daily' (days 4-7). -- temp_min_c only populated for 'daily' rows (hourly/3hourly are point-in-time). -- Each refresh cycle replaces the full set for a resolution in one transaction -- (see lib/weather-fetch.js) rather than patching individual rows, so stale points -- from a provider that later drops out of range never linger. CREATE TABLE IF NOT EXISTS forecast_points ( id SERIAL PRIMARY KEY, resolution TEXT NOT NULL, ts TIMESTAMPTZ NOT NULL, temp_c NUMERIC NOT NULL, temp_min_c NUMERIC, feels_like_c NUMERIC, precip_prob NUMERIC, wind_speed_mph NUMERIC, wind_dir_deg NUMERIC, icon TEXT NOT NULL, condition_text TEXT, source TEXT NOT NULL, fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (resolution, ts) ); CREATE INDEX IF NOT EXISTS forecast_points_res_ts_idx ON forecast_points (resolution, ts); -- Sunrise/sunset for today + the next 2 days — shown next to each day's label -- on the display. Only 3 rows at a time; replaced wholesale each refresh cycle -- alongside forecast_points (see lib/weather-fetch.js's replaceAstro()). CREATE TABLE IF NOT EXISTS forecast_astro ( date TEXT PRIMARY KEY, sunrise TIMESTAMPTZ, sunset TIMESTAMPTZ, fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- User-editable display settings (currently just display_title). Separate from -- the status table (fetch bookkeeping, not user-facing) -- same KV shape as -- tasks' config table. A missing/null display_title falls back to -- VITE_HOTEL_NAME at the frontend (see routes/config.js), so an unconfigured -- app still shows something sensible without requiring a rebuild. CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, value JSONB NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); `) // wind_dir_deg was added to forecast_points after the first deploy already ran — // CREATE TABLE IF NOT EXISTS above is a no-op against an existing table, it does // NOT retroactively add new columns, so this needs its own explicit migration. // forecast_astro/config didn't need this since they're whole new tables, which // CREATE TABLE IF NOT EXISTS does handle correctly on an existing database. await pool.query(`ALTER TABLE forecast_points ADD COLUMN IF NOT EXISTS wind_dir_deg NUMERIC`) } export async function getStatus() { const { rows } = await pool.query('SELECT key, value FROM status') return Object.fromEntries(rows.map(r => [r.key, r.value])) } export async function setStatus(key, value) { await pool.query( `INSERT INTO status (key, value, updated_at) VALUES ($1, $2, NOW()) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()`, [key, JSON.stringify(value)] ) } export async function getConfig() { const { rows } = await pool.query('SELECT key, value FROM config') return Object.fromEntries(rows.map(r => [r.key, r.value])) } export async function setConfigValue(key, value) { await pool.query( `INSERT INTO config (key, value, updated_at) VALUES ($1, $2, NOW()) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()`, [key, JSON.stringify(value)] ) }