import { pool } from '../pool.js'; const COLS = 'id, name, category, host, url, icon, check_cfg, source, enabled'; // Map a DB row to the service shape the registry/checker expect (check_cfg -> check). function toSvc(r) { return { id: r.id, name: r.name, category: r.category, host: r.host, url: r.url, icon: r.icon, check: r.check_cfg || {}, source: r.source, enabled: r.enabled }; } export async function listEnabled() { const { rows } = await pool.query( `SELECT ${COLS} FROM monitored_services WHERE enabled ORDER BY category, name`); return rows.map(toSvc); } export async function all() { const { rows } = await pool.query( `SELECT ${COLS} FROM monitored_services ORDER BY category, name`); return rows.map(toSvc); } // Discovered, not-yet-promoted candidates awaiting the owner's review. export async function listDiscovered() { const { rows } = await pool.query( `SELECT ${COLS} FROM monitored_services WHERE source='discovered' AND NOT enabled ORDER BY name`); return rows.map(toSvc); } export async function get(id) { const { rows: [r] } = await pool.query( `SELECT ${COLS} FROM monitored_services WHERE id=$1`, [id]); return r ? toSvc(r) : null; } export async function count() { const { rows: [r] } = await pool.query(`SELECT count(*)::int AS n FROM monitored_services`); return r.n; } export async function create(svc) { const { id, name, category = 'other', host = null, url, icon = null, check = {}, source = 'manual', enabled = true } = svc; const { rows: [r] } = await pool.query( `INSERT INTO monitored_services (id, name, category, host, url, icon, check_cfg, source, enabled) VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9) RETURNING ${COLS}`, [id, name, category, host, url, icon, JSON.stringify(check), source, enabled]); return toSvc(r); } const PATCHABLE = ['name', 'category', 'host', 'url', 'icon', 'enabled']; export async function update(id, patch) { const sets = [], vals = []; for (const k of PATCHABLE) { if (patch[k] !== undefined) { vals.push(patch[k]); sets.push(`${k}=$${vals.length}`); } } if (patch.check !== undefined) { vals.push(JSON.stringify(patch.check)); sets.push(`check_cfg=$${vals.length}::jsonb`); } if (!sets.length) return get(id); vals.push(id); const { rows: [r] } = await pool.query( `UPDATE monitored_services SET ${sets.join(', ')}, updated_at=now() WHERE id=$${vals.length} RETURNING ${COLS}`, vals); return r ? toSvc(r) : null; } export async function remove(id) { const { rowCount } = await pool.query(`DELETE FROM monitored_services WHERE id=$1`, [id]); return rowCount > 0; } // Insert a discovered candidate (disabled, source='discovered') unless a service // with the same id OR url already exists — never clobbers a curated entry. export async function upsertDiscovered(svc) { const { id, name, category = 'other', host = null, url, icon = null, check = {} } = svc; const { rows: [r] } = await pool.query( `INSERT INTO monitored_services (id, name, category, host, url, icon, check_cfg, source, enabled) SELECT $1,$2,$3,$4,$5,$6,$7::jsonb,'discovered',false WHERE NOT EXISTS (SELECT 1 FROM monitored_services WHERE url=$5) ON CONFLICT (id) DO NOTHING RETURNING ${COLS}`, [id, name, category, host, url, icon, JSON.stringify(check)]); return r ? toSvc(r) : null; // null = already existed (skipped) }