Initial commit: Infrastructure host tracking app
build-and-push / build-and-push (push) Successful in 1m26s
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user