From c67ac275451ba829a1685f2d052c3bba61e36989 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 2 Jun 2026 22:17:27 +1000 Subject: [PATCH] feat(dashboard): dashboard_layout table + repo Co-Authored-By: Claude Sonnet 4.6 --- lib/db/migrations/012_dashboard_layout.sql | 10 ++++++++ lib/db/repos/dashboard_layout.js | 24 +++++++++++++++++++ tests/repos/dashboard_layout.test.js | 27 ++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 lib/db/migrations/012_dashboard_layout.sql create mode 100644 lib/db/repos/dashboard_layout.js create mode 100644 tests/repos/dashboard_layout.test.js diff --git a/lib/db/migrations/012_dashboard_layout.sql b/lib/db/migrations/012_dashboard_layout.sql new file mode 100644 index 0000000..2e7c968 --- /dev/null +++ b/lib/db/migrations/012_dashboard_layout.sql @@ -0,0 +1,10 @@ +-- 012_dashboard_layout.sql +-- Single global, owner-scoped dashboard layout. One logical row keyed by a +-- stable owner key (v2 is single-owner; 'owner' is the only key for now). +CREATE TABLE dashboard_layout ( + owner_key text PRIMARY KEY DEFAULT 'owner', + card_order jsonb NOT NULL DEFAULT '[]'::jsonb, -- ["clock","weather",...] + hidden jsonb NOT NULL DEFAULT '[]'::jsonb, -- ["speedtest"] + sizes jsonb NOT NULL DEFAULT '{}'::jsonb, -- {"weather":"s"} + updated_at timestamptz NOT NULL DEFAULT now() +); diff --git a/lib/db/repos/dashboard_layout.js b/lib/db/repos/dashboard_layout.js new file mode 100644 index 0000000..11a6996 --- /dev/null +++ b/lib/db/repos/dashboard_layout.js @@ -0,0 +1,24 @@ +import { pool } from '../pool.js'; + +const DEFAULTS = { card_order: [], hidden: [], sizes: {} }; + +export async function get() { + const { rows } = await pool.query( + `SELECT card_order, hidden, sizes FROM dashboard_layout WHERE owner_key = 'owner'` + ); + return rows[0] || { ...DEFAULTS }; +} + +export async function put({ card_order = [], hidden = [], sizes = {} }) { + await pool.query( + `INSERT INTO dashboard_layout (owner_key, card_order, hidden, sizes, updated_at) + VALUES ('owner', $1::jsonb, $2::jsonb, $3::jsonb, now()) + ON CONFLICT (owner_key) DO UPDATE + SET card_order = EXCLUDED.card_order, + hidden = EXCLUDED.hidden, + sizes = EXCLUDED.sizes, + updated_at = now()`, + [JSON.stringify(card_order), JSON.stringify(hidden), JSON.stringify(sizes)] + ); + return get(); +} diff --git a/tests/repos/dashboard_layout.test.js b/tests/repos/dashboard_layout.test.js new file mode 100644 index 0000000..1365ad9 --- /dev/null +++ b/tests/repos/dashboard_layout.test.js @@ -0,0 +1,27 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { resetDb } from '../helpers/db.js'; +import { migrateUp } from '../../lib/db/migrate.js'; +import * as repo from '../../lib/db/repos/dashboard_layout.js'; + +beforeAll(async () => { await resetDb(); await migrateUp(); }); + +describe('dashboard_layout repo', () => { + it('returns defaults when unset', async () => { + const l = await repo.get(); + expect(l).toEqual({ card_order: [], hidden: [], sizes: {} }); + }); + + it('upserts and reads back', async () => { + await repo.put({ card_order: ['clock', 'weather'], hidden: ['jobs'], sizes: { weather: 's' } }); + const l = await repo.get(); + expect(l.card_order).toEqual(['clock', 'weather']); + expect(l.hidden).toEqual(['jobs']); + expect(l.sizes).toEqual({ weather: 's' }); + }); + + it('second put overwrites the same single row', async () => { + await repo.put({ card_order: ['host-perf'], hidden: [], sizes: {} }); + const l = await repo.get(); + expect(l.card_order).toEqual(['host-perf']); + }); +});