/** * @file Frontend logging routes that pipe client-side logs to the telemetry service. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { Hono } from "hono"; import { logger } from "../services/logger.js"; const validLevels = new Set([ "debug", "info", "warn" ]); const frontendRouter = new Hono(); frontendRouter.post("/log", async(context) => { try { const body = await context.req.json<{ level: string; message: string }>(); // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions -- Runtime body validation if (!body.level || !body.message || !validLevels.has(body.level)) { return context.json({ error: "level and message are required" }, 400); } /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- validated above */ void logger.log(body.level as "debug" | "info" | "warn", `[FE] ${body.message}`); return context.json({ ok: true }); } catch (error) { void logger.error( "frontend_log", error instanceof Error ? error : new Error(String(error)), ); return context.json({ error: "Internal server error" }, 500); } }); frontendRouter.post("/error", async(context) => { try { const body = await context.req.json<{ context: string; message: string }>(); // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions -- Runtime body validation if (!body.context || !body.message) { return context.json({ error: "context and message are required" }, 400); } void logger.error(`[FE] ${body.context}`, new Error(body.message)); return context.json({ ok: true }); } catch (error) { void logger.error( "frontend_error", error instanceof Error ? error : new Error(String(error)), ); return context.json({ error: "Internal server error" }, 500); } }); export { frontendRouter };