diff --git a/config/services.json b/config/services.json new file mode 100644 index 0000000..2b6a465 --- /dev/null +++ b/config/services.json @@ -0,0 +1,11 @@ +[ + { "id": "void-server", "name": "Void 2.0", "category": "agents", "host": "ct311", "url": "http://192.168.1.216:3000", "icon": "void", "check": { "type": "http", "path": "/health" } }, + { "id": "ollama", "name": "Ollama", "category": "agents", "host": "ct102", "url": "http://192.168.1.185:11434", "icon": "ollama" }, + { "id": "gitea", "name": "Gitea", "category": "infrastructure", "host": "ct105", "url": "http://192.168.1.223:3000", "icon": "gitea" }, + { "id": "pihole", "name": "Pi-hole", "category": "infrastructure", "host": "ct106", "url": "http://192.168.1.140/admin", "icon": "pi-hole" }, + { "id": "bookstack", "name": "BookStack", "category": "infrastructure", "host": "ct104", "url": "http://192.168.1.213", "icon": "bookstack" }, + { "id": "plex", "name": "Plex", "category": "media", "host": "ct100", "url": "http://192.168.1.230:32400/web", "icon": "plex" }, + { "id": "sonarr", "name": "Sonarr", "category": "media", "host": "ct100", "url": "http://192.168.1.230:8989", "icon": "sonarr" }, + { "id": "radarr", "name": "Radarr", "category": "media", "host": "ct100", "url": "http://192.168.1.230:7878", "icon": "radarr" }, + { "id": "qbittorrent", "name": "qBittorrent", "category": "media", "host": "ct100", "url": "http://192.168.1.230:8080", "icon": "qbittorrent" } +] diff --git a/lib/health/registry.js b/lib/health/registry.js new file mode 100644 index 0000000..86fcd6c --- /dev/null +++ b/lib/health/registry.js @@ -0,0 +1,30 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const CONFIG = path.join(__dirname, '../../config/services.json'); +export const CATEGORY_ORDER = ['agents', 'infrastructure', 'media', 'other']; + +let cache = null; +export function load() { + if (!cache) cache = JSON.parse(readFileSync(CONFIG, 'utf8')); + return cache; +} +export function _reset() { cache = null; } // tests + +export function iconSlug(svc) { + return (svc.icon || svc.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); +} + +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) })); +} diff --git a/tests/health/registry.test.js b/tests/health/registry.test.js new file mode 100644 index 0000000..d7ddec8 --- /dev/null +++ b/tests/health/registry.test.js @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { load, grouped, iconSlug, CATEGORY_ORDER } from '../../lib/health/registry.js'; + +describe('registry', () => { + it('loads the seed config', () => { expect(load().length).toBeGreaterThan(0); }); + it('derives an icon slug from icon or name', () => { + expect(iconSlug({ name: 'Open WebUI' })).toBe('open-webui'); + expect(iconSlug({ name: 'Plex', icon: 'plex' })).toBe('plex'); + }); + it('groups in agents→infrastructure→media order', () => { + const g = grouped(load()); + const cats = g.map(x => x.category); + const ai = cats.indexOf('agents'), mi = cats.indexOf('media'); + expect(ai).toBeLessThan(mi); + expect(CATEGORY_ORDER[0]).toBe('agents'); + }); +});