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,88 @@
|
||||
import { schemas } from '../schemas.js';
|
||||
import { translateSqliteError } from '../sqlite-errors.js';
|
||||
|
||||
export default async function hostsRoutes(fastify) {
|
||||
const { db } = fastify;
|
||||
|
||||
fastify.get('/', {
|
||||
schema: {
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: { q: { type: 'string' } },
|
||||
},
|
||||
response: {
|
||||
200: { type: 'array', items: schemas.hostResponse },
|
||||
},
|
||||
},
|
||||
}, async (req) => {
|
||||
const q = (req.query.q ?? '').trim();
|
||||
return q ? db.hosts.search(q) : db.hosts.list();
|
||||
});
|
||||
|
||||
fastify.get('/by-hardware-id/:hardwareId', {
|
||||
schema: {
|
||||
params: {
|
||||
type: 'object',
|
||||
required: ['hardwareId'],
|
||||
properties: { hardwareId: { type: 'string', minLength: 1 } },
|
||||
},
|
||||
response: { 200: schemas.hostResponse, 404: schemas.errorResponse },
|
||||
},
|
||||
}, async (req) => {
|
||||
const host = db.hosts.getByHardwareId(req.params.hardwareId);
|
||||
if (!host) throw fastify.httpErrors.notFound('host not found');
|
||||
return host;
|
||||
});
|
||||
|
||||
fastify.get('/:id', {
|
||||
schema: {
|
||||
params: schemas.idParam,
|
||||
response: { 200: schemas.hostResponse, 404: schemas.errorResponse },
|
||||
},
|
||||
}, async (req) => {
|
||||
const host = db.hosts.get(req.params.id);
|
||||
if (!host) throw fastify.httpErrors.notFound('host not found');
|
||||
return host;
|
||||
});
|
||||
|
||||
fastify.post('/', {
|
||||
schema: {
|
||||
body: schemas.hostBody,
|
||||
response: { 201: schemas.hostResponse },
|
||||
},
|
||||
}, async (req, reply) => {
|
||||
try {
|
||||
const host = db.hosts.create(req.body);
|
||||
reply.code(201);
|
||||
return host;
|
||||
} catch (err) {
|
||||
translateSqliteError(err, fastify);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.put('/:id', {
|
||||
schema: {
|
||||
params: schemas.idParam,
|
||||
body: schemas.hostBody,
|
||||
response: { 200: schemas.hostResponse, 404: schemas.errorResponse },
|
||||
},
|
||||
}, async (req) => {
|
||||
try {
|
||||
const host = db.hosts.update(req.params.id, req.body);
|
||||
if (!host) throw fastify.httpErrors.notFound('host not found');
|
||||
return host;
|
||||
} catch (err) {
|
||||
if (err.statusCode) throw err;
|
||||
translateSqliteError(err, fastify);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.delete('/:id', {
|
||||
schema: { params: schemas.idParam },
|
||||
}, async (req, reply) => {
|
||||
const removed = db.hosts.delete(req.params.id);
|
||||
if (!removed) throw fastify.httpErrors.notFound('host not found');
|
||||
reply.code(204);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { schemas } from '../schemas.js';
|
||||
import { translateSqliteError } from '../sqlite-errors.js';
|
||||
|
||||
export default async function interfacesRoutes(fastify) {
|
||||
const { db } = fastify;
|
||||
|
||||
fastify.get('/', {
|
||||
schema: {
|
||||
querystring: schemas.interfaceQuery,
|
||||
response: { 200: { type: 'array', items: schemas.interfaceResponse } },
|
||||
},
|
||||
}, async (req) => db.interfaces.listByHost(req.query.host_id));
|
||||
|
||||
fastify.get('/:id', {
|
||||
schema: {
|
||||
params: schemas.idParam,
|
||||
response: { 200: schemas.interfaceResponse, 404: schemas.errorResponse },
|
||||
},
|
||||
}, async (req) => {
|
||||
const row = db.interfaces.get(req.params.id);
|
||||
if (!row) throw fastify.httpErrors.notFound('interface not found');
|
||||
return row;
|
||||
});
|
||||
|
||||
fastify.post('/', {
|
||||
schema: {
|
||||
body: schemas.interfaceBody,
|
||||
response: { 201: schemas.interfaceResponse },
|
||||
},
|
||||
}, async (req, reply) => {
|
||||
try {
|
||||
const row = db.interfaces.create(req.body);
|
||||
reply.code(201);
|
||||
return row;
|
||||
} catch (err) {
|
||||
translateSqliteError(err, fastify, {
|
||||
uniqueMessage: 'an interface with that name already exists on this host',
|
||||
foreignKeyMessage: 'host does not exist',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fastify.put('/:id', {
|
||||
schema: {
|
||||
params: schemas.idParam,
|
||||
body: schemas.interfaceBody,
|
||||
response: { 200: schemas.interfaceResponse, 404: schemas.errorResponse },
|
||||
},
|
||||
}, async (req) => {
|
||||
try {
|
||||
const row = db.interfaces.update(req.params.id, req.body);
|
||||
if (!row) throw fastify.httpErrors.notFound('interface not found');
|
||||
return row;
|
||||
} catch (err) {
|
||||
if (err.statusCode) throw err;
|
||||
translateSqliteError(err, fastify, {
|
||||
uniqueMessage: 'an interface with that name already exists on this host',
|
||||
foreignKeyMessage: 'host does not exist',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fastify.delete('/:id', {
|
||||
schema: { params: schemas.idParam },
|
||||
}, async (req, reply) => {
|
||||
const removed = db.interfaces.delete(req.params.id);
|
||||
if (!removed) throw fastify.httpErrors.notFound('interface not found');
|
||||
reply.code(204);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { schemas } from '../schemas.js';
|
||||
import { translateSqliteError } from '../sqlite-errors.js';
|
||||
|
||||
export default async function roomsRoutes(fastify) {
|
||||
const { db } = fastify;
|
||||
|
||||
fastify.get('/', {
|
||||
schema: {
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: { site_id: { type: 'integer', minimum: 1 } },
|
||||
},
|
||||
response: { 200: { type: 'array', items: schemas.roomResponse } },
|
||||
},
|
||||
}, async (req) => db.rooms.list(req.query.site_id));
|
||||
|
||||
fastify.get('/:id', {
|
||||
schema: { params: schemas.idParam, response: { 200: schemas.roomResponse } },
|
||||
}, async (req) => {
|
||||
const row = db.rooms.get(req.params.id);
|
||||
if (!row) throw fastify.httpErrors.notFound('room not found');
|
||||
return row;
|
||||
});
|
||||
|
||||
fastify.post('/', {
|
||||
schema: { body: schemas.roomBody, response: { 201: schemas.roomResponse } },
|
||||
}, async (req, reply) => {
|
||||
try {
|
||||
const row = db.rooms.create(req.body.site_id, req.body.name);
|
||||
reply.code(201);
|
||||
return row;
|
||||
} catch (err) {
|
||||
translateSqliteError(err, fastify, {
|
||||
uniqueMessage: 'a room with that name already exists at this site',
|
||||
foreignKeyMessage: 'site does not exist',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fastify.put('/:id', {
|
||||
schema: { params: schemas.idParam, body: schemas.roomBody, response: { 200: schemas.roomResponse } },
|
||||
}, async (req) => {
|
||||
try {
|
||||
const row = db.rooms.update(req.params.id, req.body.site_id, req.body.name);
|
||||
if (!row) throw fastify.httpErrors.notFound('room not found');
|
||||
return row;
|
||||
} catch (err) {
|
||||
if (err.statusCode) throw err;
|
||||
translateSqliteError(err, fastify, {
|
||||
uniqueMessage: 'a room with that name already exists at this site',
|
||||
foreignKeyMessage: 'site does not exist',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fastify.delete('/:id', {
|
||||
schema: { params: schemas.idParam },
|
||||
}, async (req, reply) => {
|
||||
try {
|
||||
const removed = db.rooms.delete(req.params.id);
|
||||
if (!removed) throw fastify.httpErrors.notFound('room not found');
|
||||
reply.code(204);
|
||||
return null;
|
||||
} catch (err) {
|
||||
if (err.statusCode) throw err;
|
||||
translateSqliteError(err, fastify, { foreignKeyMessage: 'cannot delete: hosts still reference this room' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { schemas } from '../schemas.js';
|
||||
import { translateSqliteError } from '../sqlite-errors.js';
|
||||
|
||||
export default async function serverTypesRoutes(fastify) {
|
||||
const { db } = fastify;
|
||||
|
||||
fastify.get('/', {
|
||||
schema: { response: { 200: { type: 'array', items: schemas.lookupResponse } } },
|
||||
}, async () => db.serverTypes.list());
|
||||
|
||||
fastify.get('/:id', {
|
||||
schema: { params: schemas.idParam, response: { 200: schemas.lookupResponse } },
|
||||
}, async (req) => {
|
||||
const row = db.serverTypes.get(req.params.id);
|
||||
if (!row) throw fastify.httpErrors.notFound('server type not found');
|
||||
return row;
|
||||
});
|
||||
|
||||
fastify.post('/', {
|
||||
schema: { body: schemas.lookupBody, response: { 201: schemas.lookupResponse } },
|
||||
}, async (req, reply) => {
|
||||
try {
|
||||
const row = db.serverTypes.create(req.body.name);
|
||||
reply.code(201);
|
||||
return row;
|
||||
} catch (err) {
|
||||
translateSqliteError(err, fastify, { uniqueMessage: 'a server type with that name already exists' });
|
||||
}
|
||||
});
|
||||
|
||||
fastify.put('/:id', {
|
||||
schema: { params: schemas.idParam, body: schemas.lookupBody, response: { 200: schemas.lookupResponse } },
|
||||
}, async (req) => {
|
||||
try {
|
||||
const row = db.serverTypes.update(req.params.id, req.body.name);
|
||||
if (!row) throw fastify.httpErrors.notFound('server type not found');
|
||||
return row;
|
||||
} catch (err) {
|
||||
if (err.statusCode) throw err;
|
||||
translateSqliteError(err, fastify, { uniqueMessage: 'a server type with that name already exists' });
|
||||
}
|
||||
});
|
||||
|
||||
fastify.delete('/:id', {
|
||||
schema: { params: schemas.idParam },
|
||||
}, async (req, reply) => {
|
||||
try {
|
||||
const removed = db.serverTypes.delete(req.params.id);
|
||||
if (!removed) throw fastify.httpErrors.notFound('server type not found');
|
||||
reply.code(204);
|
||||
return null;
|
||||
} catch (err) {
|
||||
if (err.statusCode) throw err;
|
||||
translateSqliteError(err, fastify, { foreignKeyMessage: 'cannot delete: hosts still reference this server type' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { schemas } from '../schemas.js';
|
||||
import { translateSqliteError } from '../sqlite-errors.js';
|
||||
|
||||
export default async function sitesRoutes(fastify) {
|
||||
const { db } = fastify;
|
||||
|
||||
fastify.get('/', {
|
||||
schema: { response: { 200: { type: 'array', items: schemas.lookupResponse } } },
|
||||
}, async () => db.sites.list());
|
||||
|
||||
fastify.get('/:id', {
|
||||
schema: { params: schemas.idParam, response: { 200: schemas.lookupResponse } },
|
||||
}, async (req) => {
|
||||
const row = db.sites.get(req.params.id);
|
||||
if (!row) throw fastify.httpErrors.notFound('site not found');
|
||||
return row;
|
||||
});
|
||||
|
||||
fastify.post('/', {
|
||||
schema: { body: schemas.lookupBody, response: { 201: schemas.lookupResponse } },
|
||||
}, async (req, reply) => {
|
||||
try {
|
||||
const row = db.sites.create(req.body.name);
|
||||
reply.code(201);
|
||||
return row;
|
||||
} catch (err) {
|
||||
translateSqliteError(err, fastify, { uniqueMessage: 'a site with that name already exists' });
|
||||
}
|
||||
});
|
||||
|
||||
fastify.put('/:id', {
|
||||
schema: { params: schemas.idParam, body: schemas.lookupBody, response: { 200: schemas.lookupResponse } },
|
||||
}, async (req) => {
|
||||
try {
|
||||
const row = db.sites.update(req.params.id, req.body.name);
|
||||
if (!row) throw fastify.httpErrors.notFound('site not found');
|
||||
return row;
|
||||
} catch (err) {
|
||||
if (err.statusCode) throw err;
|
||||
translateSqliteError(err, fastify, { uniqueMessage: 'a site with that name already exists' });
|
||||
}
|
||||
});
|
||||
|
||||
fastify.delete('/:id', {
|
||||
schema: { params: schemas.idParam },
|
||||
}, async (req, reply) => {
|
||||
try {
|
||||
const removed = db.sites.delete(req.params.id);
|
||||
if (!removed) throw fastify.httpErrors.notFound('site not found');
|
||||
reply.code(204);
|
||||
return null;
|
||||
} catch (err) {
|
||||
if (err.statusCode) throw err;
|
||||
translateSqliteError(err, fastify, { foreignKeyMessage: 'cannot delete: rooms still reference this site' });
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user