f500db971b
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>
82 lines
2.6 KiB
JavaScript
82 lines
2.6 KiB
JavaScript
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);
|
|
}
|
|
}
|