feat: 2.0.0-alpha.11 — DB-backed service registry + LAN auto-discovery

- monitored_services table (mig 015) replaces config/services.json (now a boot seed)
- owner CRUD over /api/health/services; GET is DB-backed; cron+worker read the DB
- discover.lan worker: pure-Node TCP sweep + HTTP-title probe -> disabled 'discovered'
  candidates (never clobbers curated entries); POST /api/health/discover + GET .../discovered
- dashboard: Scan button + Discovered(N) section with one-click promote

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-04 07:55:08 +10:00
parent b728696020
commit ce26895d8e
17 changed files with 466 additions and 46 deletions

View File

@@ -0,0 +1,48 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { resetDb } from '../helpers/db.js';
import { migrateUp } from '../../lib/db/migrate.js';
import * as repo from '../../lib/db/repos/monitored_services.js';
beforeAll(async () => { await resetDb(); await migrateUp(); });
beforeEach(async () => { await resetDb(); await migrateUp(); });
const gitea = { id: 'gitea', name: 'Gitea', category: 'infrastructure', host: 'ct105', url: 'http://192.168.1.223:3000', icon: 'gitea', check: { type: 'http' } };
describe('monitored_services repo', () => {
it('creates and lists enabled with check_cfg mapped to check', async () => {
await repo.create(gitea);
const list = await repo.listEnabled();
expect(list).toHaveLength(1);
expect(list[0].id).toBe('gitea');
expect(list[0].check).toEqual({ type: 'http' }); // check_cfg -> check
expect(list[0].source).toBe('manual');
});
it('update patches fields (incl. check + enabled)', async () => {
await repo.create(gitea);
await repo.update('gitea', { name: 'Gitea CE', enabled: false, check: { type: 'tcp' } });
expect((await repo.get('gitea')).name).toBe('Gitea CE');
expect(await repo.listEnabled()).toHaveLength(0); // now disabled
expect((await repo.get('gitea')).check).toEqual({ type: 'tcp' });
});
it('remove deletes', async () => {
await repo.create(gitea);
expect(await repo.remove('gitea')).toBe(true);
expect(await repo.get('gitea')).toBeNull();
});
it('upsertDiscovered adds a disabled candidate, but never clobbers an existing url/id', async () => {
await repo.create(gitea); // manual, enabled
// same url -> skipped
expect(await repo.upsertDiscovered({ id: 'gitea-x', name: 'g', url: gitea.url })).toBeNull();
// new url -> inserted, disabled, source=discovered
const d = await repo.upsertDiscovered({ id: 'plex', name: 'Plex?', url: 'http://192.168.1.230:32400' });
expect(d.source).toBe('discovered');
expect(d.enabled).toBe(false);
expect(await repo.listEnabled()).toHaveLength(1); // only gitea
expect(await repo.listDiscovered()).toHaveLength(1); // plex candidate
// re-running discovery is idempotent (same id -> skipped)
expect(await repo.upsertDiscovered({ id: 'plex', name: 'Plex?', url: 'http://192.168.1.230:32400' })).toBeNull();
});
});