import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import * as repo from '../db/repos/monitored_services.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const SEED_FILE = path.join(__dirname, '../../config/services.json'); export const CATEGORY_ORDER = ['agents', 'infrastructure', 'media', 'other']; // Icon slug: explicit `icon`, else slugified name. Pure. export function iconSlug(svc) { return (svc.icon || svc.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); } // Group services by category in CATEGORY_ORDER (unknown categories last). Pure. export function grouped(services) { const map = new Map(); for (const s of services) { const cat = CATEGORY_ORDER.includes(s.category) ? s.category : 'other'; if (!map.has(cat)) map.set(cat, []); map.get(cat).push(s); } return [...CATEGORY_ORDER, ...[...map.keys()].filter(c => !CATEGORY_ORDER.includes(c))] .filter(c => map.has(c)) .map(category => ({ category, services: map.get(category) })); } // One-time bootstrap: if the registry table is empty, populate it from the // version-controlled config/services.json seed. Idempotent (no-op once seeded). export async function seedFromConfig() { if ((await repo.count()) > 0) return 0; let seed; try { seed = JSON.parse(readFileSync(SEED_FILE, 'utf8')); } catch { return 0; } let n = 0; for (const s of seed) { try { await repo.create({ ...s, source: 'manual', enabled: true }); n++; } catch { /* skip a bad/duplicate seed row */ } } return n; }