feat(health): service registry loader + seed config (fresh titles)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-02 22:52:46 +10:00
parent 8d1950fcaa
commit 3ea34d9907
3 changed files with 58 additions and 0 deletions

11
config/services.json Normal file
View File

@@ -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" }
]

30
lib/health/registry.js Normal file
View File

@@ -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) }));
}

View File

@@ -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');
});
});