Initial commit: Infrastructure host tracking app
build-and-push / build-and-push (push) Successful in 1m26s

Fastify + node:sqlite single-process app with vanilla JS UI for
looking up hosts by hardware ID, hostname, or asset ID. Includes
per-host network interface tracking, sites/rooms/server-types CRUD,
Docker packaging, and a Gitea Actions workflow that runs tests then
builds and pushes to gitea.thewrightserver.net/josh/infrastructure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 17:05:50 -04:00
commit f500db971b
26 changed files with 4057 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
import { test, after } from 'node:test';
import assert from 'node:assert/strict';
import { newApp, seedFixtures, newHostPayload } from './helpers.js';
test('hosts: create, get, search, update, delete', async (t) => {
const app = await newApp();
after(() => app.close());
const fx = await seedFixtures(app);
// Create
const create = await app.inject({
method: 'POST', url: '/api/hosts',
payload: newHostPayload({ room_id: fx.room.id, server_type_id: fx.type.id }, 'A'),
});
assert.equal(create.statusCode, 201);
const created = JSON.parse(create.body);
assert.equal(created.hostname, 'host-A');
assert.equal(created.site_name, 'HQ');
assert.equal(created.room_name, 'Main');
assert.equal(created.server_type, 'Web');
// Get one
const get = await app.inject({ method: 'GET', url: `/api/hosts/${created.id}` });
assert.equal(get.statusCode, 200);
assert.equal(JSON.parse(get.body).hostname, 'host-A');
// 404 on missing
const missing = await app.inject({ method: 'GET', url: '/api/hosts/9999' });
assert.equal(missing.statusCode, 404);
// Lookup by hardware_id
const byHwid = await app.inject({ method: 'GET', url: '/api/hosts/by-hardware-id/HW-A' });
assert.equal(byHwid.statusCode, 200);
assert.equal(JSON.parse(byHwid.body).id, created.id);
// 404 by hardware_id
const missingHwid = await app.inject({ method: 'GET', url: '/api/hosts/by-hardware-id/nope' });
assert.equal(missingHwid.statusCode, 404);
// Search by hostname / hardware_id / asset_id
for (const q of ['host', 'HW-A', 'AST-A']) {
const r = await app.inject({ method: 'GET', url: `/api/hosts?q=${encodeURIComponent(q)}` });
assert.equal(r.statusCode, 200);
const rows = JSON.parse(r.body);
assert.equal(rows.length, 1);
assert.equal(rows[0].hostname, 'host-A');
}
// Search is case-insensitive
const ci = await app.inject({ method: 'GET', url: '/api/hosts?q=HOST-a' });
assert.equal(JSON.parse(ci.body).length, 1);
// Update
const upd = await app.inject({
method: 'PUT', url: `/api/hosts/${created.id}`,
payload: { ...newHostPayload({ room_id: fx.room.id, server_type_id: fx.type.id }, 'A'), position: 'R5-U10' },
});
assert.equal(upd.statusCode, 200);
assert.equal(JSON.parse(upd.body).position, 'R5-U10');
// Delete
const del = await app.inject({ method: 'DELETE', url: `/api/hosts/${created.id}` });
assert.equal(del.statusCode, 204);
const afterDel = await app.inject({ method: 'GET', url: `/api/hosts/${created.id}` });
assert.equal(afterDel.statusCode, 404);
});
test('hosts: duplicate hardware_id returns 409', async (t) => {
const app = await newApp();
after(() => app.close());
const fx = await seedFixtures(app);
const a = await app.inject({
method: 'POST', url: '/api/hosts',
payload: newHostPayload({ room_id: fx.room.id, server_type_id: fx.type.id }, 'X'),
});
assert.equal(a.statusCode, 201);
const dup = await app.inject({
method: 'POST', url: '/api/hosts',
payload: { ...newHostPayload({ room_id: fx.room.id, server_type_id: fx.type.id }, 'Y'), hardware_id: 'HW-X' },
});
assert.equal(dup.statusCode, 409);
assert.match(JSON.parse(dup.body).error, /hardware_id/);
});
test('hosts: invalid body returns 400 with details', async (t) => {
const app = await newApp();
after(() => app.close());
const r = await app.inject({
method: 'POST', url: '/api/hosts',
payload: { hostname: '' },
});
assert.equal(r.statusCode, 400);
const body = JSON.parse(r.body);
assert.equal(body.error, 'validation failed');
assert.ok(Array.isArray(body.details) && body.details.length > 0);
});
test('hosts: missing FK returns 409', async (t) => {
const app = await newApp();
after(() => app.close());
const r = await app.inject({
method: 'POST', url: '/api/hosts',
payload: {
hardware_id: 'HW-1', hostname: 'h', asset_id: 'A',
room_id: 999, position: '', server_type_id: 999,
},
});
assert.equal(r.statusCode, 409);
});
test('hosts: list caps at 200', async (t) => {
const app = await newApp();
after(() => app.close());
const fx = await seedFixtures(app);
for (let i = 0; i < 205; i++) {
await app.inject({
method: 'POST', url: '/api/hosts',
payload: newHostPayload({ room_id: fx.room.id, server_type_id: fx.type.id }, String(i).padStart(4, '0')),
});
}
const r = await app.inject({ method: 'GET', url: '/api/hosts' });
assert.equal(JSON.parse(r.body).length, 200);
});