feat(health): service registry loader + seed config (fresh titles)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
11
config/services.json
Normal file
11
config/services.json
Normal 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
30
lib/health/registry.js
Normal 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) }));
|
||||
}
|
||||
17
tests/health/registry.test.js
Normal file
17
tests/health/registry.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user