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
+81
View File
@@ -0,0 +1,81 @@
import Fastify from 'fastify';
import sensible from '@fastify/sensible';
import fastifyStatic from '@fastify/static';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';
import { openDb, seedIfEmpty } from './db.js';
import hostsRoutes from './routes/hosts.js';
import sitesRoutes from './routes/sites.js';
import roomsRoutes from './routes/rooms.js';
import serverTypesRoutes from './routes/server-types.js';
import interfacesRoutes from './routes/interfaces.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PUBLIC_DIR = join(__dirname, '../public');
const DEFAULT_DB = join(__dirname, '../data/infrastructure.db');
export async function buildApp(opts = {}) {
const dbPath = opts.dbPath ?? process.env.DB_PATH ?? DEFAULT_DB;
const db = openDb(dbPath);
if (opts.seed !== false) seedIfEmpty(db);
const app = Fastify({
logger: opts.logger ?? false,
});
app.decorate('db', db);
app.addHook('onClose', (instance, done) => {
instance.db.close();
done();
});
await app.register(sensible);
app.setErrorHandler((err, req, reply) => {
if (err.validation) {
const details = err.validation.map((v) => `${v.instancePath || '/'} ${v.message}`);
return reply.code(400).send({ error: 'validation failed', details });
}
if (err.statusCode && err.statusCode < 500) {
return reply.code(err.statusCode).send({ error: err.message });
}
req.log?.error(err);
return reply.code(500).send({ error: 'internal server error' });
});
app.setNotFoundHandler(async (req, reply) => {
if (req.url.startsWith('/api/')) {
return reply.code(404).send({ error: 'not found' });
}
return reply.sendFile('index.html');
});
await app.register(async (api) => {
await api.register(hostsRoutes, { prefix: '/hosts' });
await api.register(sitesRoutes, { prefix: '/sites' });
await api.register(roomsRoutes, { prefix: '/rooms' });
await api.register(serverTypesRoutes, { prefix: '/server-types' });
await api.register(interfacesRoutes, { prefix: '/interfaces' });
}, { prefix: '/api' });
await app.register(fastifyStatic, {
root: PUBLIC_DIR,
prefix: '/',
});
return app;
}
const isMain = import.meta.url === pathToFileURL(process.argv[1] ?? '').href;
if (isMain) {
const port = Number(process.env.PORT ?? 3000);
const host = process.env.HOST ?? '0.0.0.0';
const app = await buildApp({ logger: true });
try {
await app.listen({ port, host });
} catch (err) {
app.log.error(err);
process.exit(1);
}
}