From 828adcfcc215ec5a07a6a1029882736a756e4eb4 Mon Sep 17 00:00:00 2001 From: Flavio Fois Date: Mon, 16 Feb 2026 08:54:29 +0100 Subject: [PATCH] feat: add heartbeat check for bug report API and enhance logging throughout the application --- DOCUMENTATION.md | 15 +++- app_bugreport.go | 19 +++-- app_heartbeat.go | 45 ++++++++++ config.ini | 2 +- server/compose-dev.yml | 85 +++++++++++++++++++ .../{docker-compose.yml => compose-prod.yml} | 5 +- server/dashboard/src/hooks.server.ts | 13 +++ server/dashboard/src/lib/server/logger.ts | 42 +++++++++ server/src/db/migrate.ts | 5 +- server/src/index.ts | 25 +++++- server/src/logger.ts | 42 +++++++++ server/src/middleware/auth.ts | 15 +++- server/src/middleware/rateLimit.ts | 2 + server/src/routes/admin.ts | 5 ++ server/src/routes/bugReports.ts | 18 ++-- 15 files changed, 312 insertions(+), 26 deletions(-) create mode 100644 app_heartbeat.go create mode 100644 server/compose-dev.yml rename server/{docker-compose.yml => compose-prod.yml} (95%) create mode 100644 server/dashboard/src/lib/server/logger.ts create mode 100644 server/src/logger.ts diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 2c6eedb..96ce039 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -81,6 +81,7 @@ EMLy/ ├── app_viewer.go # Viewer window management (image, PDF, EML) ├── app_screenshot.go # Screenshot capture functionality ├── app_bugreport.go # Bug report creation and submission +├── app_heartbeat.go # Bug report API heartbeat check ├── app_settings.go # Settings import/export ├── app_system.go # Windows system utilities (registry, encoding) ├── main.go # Application entry point @@ -199,6 +200,7 @@ The Go backend is split into logical files: | `app_viewer.go` | Viewer windows: `OpenImageWindow`, `OpenPDFWindow`, `OpenEMLWindow`, `OpenPDF`, `OpenImage`, `GetViewerData` | | `app_screenshot.go` | Screenshots: `TakeScreenshot`, `SaveScreenshot`, `SaveScreenshotAs` | | `app_bugreport.go` | Bug reports: `CreateBugReportFolder`, `SubmitBugReport`, `zipFolder` | +| `app_heartbeat.go` | API heartbeat: `CheckBugReportAPI` | | `app_settings.go` | Settings I/O: `ExportSettings`, `ImportSettings` | | `app_system.go` | System utilities: `CheckIsDefaultEMLHandler`, `OpenDefaultAppsSettings`, `ConvertToUTF8`, `OpenFolderInExplorer` | | `app_update.go` | Update system: `CheckForUpdates`, `DownloadUpdate`, `InstallUpdate`, `GetUpdateStatus` | @@ -254,6 +256,7 @@ The Go backend is split into logical files: | `CreateBugReportFolder()` | Creates folder with screenshot and mail file | | `SubmitBugReport(input)` | Creates complete bug report with ZIP archive, attempts server upload | | `UploadBugReport(folderPath, input)` | Uploads bug report files to configured API server via multipart POST | +| `CheckBugReportAPI()` | Checks if the bug report API is reachable via /health endpoint (3s timeout) | **Settings (`app_settings.go`)** @@ -673,9 +676,14 @@ Complete bug reporting system: 3. Includes current mail file if loaded 4. Gathers system information 5. Creates ZIP archive in temp folder -6. Attempts to upload to the bug report API server (if configured) -7. Falls back to local ZIP if server is unreachable -8. Shows server confirmation with report ID, or local path with upload warning +6. Checks if the bug report API is online via heartbeat (`CheckBugReportAPI`) +7. If online, attempts to upload to the bug report API server +8. Falls back to local ZIP if server is offline or upload fails +9. Shows server confirmation with report ID, or local path with upload warning + +#### Heartbeat Check (`app_heartbeat.go`) + +Before uploading a bug report, the app sends a GET request to `{BUGREPORT_API_URL}/health` with a 3-second timeout. If the API doesn't respond with status 200, the upload is skipped entirely and only the local ZIP is created. The `CheckBugReportAPI()` method is also exposed to the frontend for UI status checks. #### Bug Report API Server @@ -684,6 +692,7 @@ A separate API server (`server/` directory) receives bug reports: - **Deployment**: Docker Compose (`docker compose up -d` from `server/`) - **Auth**: Static API key for clients (`X-API-Key`), static admin key (`X-Admin-Key`) - **Rate limiting**: HWID-based, configurable (default 5 reports per 24h) +- **Logging**: Structured file logging to `logs/api.log` with format `[date] - [time] - [source] - message` - **Endpoints**: `POST /api/bug-reports` (client), `GET/DELETE /api/admin/bug-reports` (admin) #### Bug Report Dashboard diff --git a/app_bugreport.go b/app_bugreport.go index 204187e..859b6e5 100644 --- a/app_bugreport.go +++ b/app_bugreport.go @@ -249,14 +249,19 @@ External IP: %s FolderPath: bugReportFolder, } - // Attempt to upload to the bug report API server - reportID, uploadErr := a.UploadBugReport(bugReportFolder, input) - if uploadErr != nil { - Log("Bug report upload failed (falling back to local zip):", uploadErr) - result.UploadError = uploadErr.Error() + // Attempt to upload to the bug report API server (only if reachable) + if !a.CheckBugReportAPI() { + Log("Bug report API is offline, skipping upload") + result.UploadError = "Bug report API is offline" } else { - result.Uploaded = true - result.ReportID = reportID + reportID, uploadErr := a.UploadBugReport(bugReportFolder, input) + if uploadErr != nil { + Log("Bug report upload failed (falling back to local zip):", uploadErr) + result.UploadError = uploadErr.Error() + } else { + result.Uploaded = true + result.ReportID = reportID + } } return result, nil diff --git a/app_heartbeat.go b/app_heartbeat.go new file mode 100644 index 0000000..cf592dd --- /dev/null +++ b/app_heartbeat.go @@ -0,0 +1,45 @@ +// Package main provides heartbeat checking for the bug report API. +package main + +import ( + "fmt" + "net/http" + "time" + + "emly/backend/utils" +) + +// CheckBugReportAPI sends a GET request to the bug report API's /health +// endpoint with a short timeout. Returns true if the API responds with +// status 200, false otherwise. This is exposed to the frontend. +func (a *App) CheckBugReportAPI() bool { + cfgPath := utils.DefaultConfigPath() + cfg, err := utils.LoadConfig(cfgPath) + if err != nil { + Log("Heartbeat: failed to load config:", err) + return false + } + + apiURL := cfg.EMLy.BugReportAPIURL + if apiURL == "" { + Log("Heartbeat: bug report API URL not configured") + return false + } + + endpoint := apiURL + "/health" + client := &http.Client{Timeout: 3 * time.Second} + + resp, err := client.Get(endpoint) + if err != nil { + Log("Heartbeat: API unreachable:", err) + return false + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + Log(fmt.Sprintf("Heartbeat: API returned status %d", resp.StatusCode)) + return false + } + + return true +} diff --git a/config.ini b/config.ini index 2d8dec6..fa84ec1 100644 --- a/config.ini +++ b/config.ini @@ -7,5 +7,5 @@ LANGUAGE = it UPDATE_CHECK_ENABLED = false UPDATE_PATH = UPDATE_AUTO_CHECK = false -BUGREPORT_API_URL = "https://api.whiskr.it" +BUGREPORT_API_URL = "https://emly-api.lyzcoote.cloud" BUGREPORT_API_KEY = "emly_1BaQdBknsMGcY5DynSby71JnWOKXtJvnuUprkgWT0pujpLFxj5HaTXP9vtJAMk63" \ No newline at end of file diff --git a/server/compose-dev.yml b/server/compose-dev.yml new file mode 100644 index 0000000..f97f940 --- /dev/null +++ b/server/compose-dev.yml @@ -0,0 +1,85 @@ +services: + mysql: + image: mysql:lts + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE:-emly_bugreports} + MYSQL_USER: ${MYSQL_USER:-emly} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - mysql_data:/var/lib/mysql + ports: + - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + emly: + ipv4_address: 172.16.32.2 + + api: + build: . + environment: + MYSQL_HOST: mysql + MYSQL_PORT: 3306 + MYSQL_USER: ${MYSQL_USER:-emly} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE:-emly_bugreports} + API_KEY: ${API_KEY} + ADMIN_KEY: ${ADMIN_KEY} + PORT: 3000 + RATE_LIMIT_MAX: ${RATE_LIMIT_MAX:-5} + RATE_LIMIT_WINDOW_HOURS: ${RATE_LIMIT_WINDOW_HOURS:-24} + volumes: + - ./logs/api:/app/logs + restart: on-failure + depends_on: + mysql: + condition: service_healthy + networks: + emly: + ipv4_address: 172.16.32.3 + + dashboard: + build: ./dashboard + environment: + MYSQL_HOST: mysql + MYSQL_PORT: 3306 + MYSQL_USER: ${MYSQL_USER:-emly} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE:-emly_bugreports} + volumes: + - ./logs/dashboard:/app/logs + depends_on: + mysql: + condition: service_healthy + networks: + emly: + ipv4_address: 172.16.32.4 + + cloudflared: + image: cloudflare/cloudflared:latest + command: tunnel run + environment: + TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN_DEV} + depends_on: + - api + - dashboard + restart: unless-stopped + networks: + emly: + ipv4_address: 172.16.32.5 + +volumes: + mysql_data: + +networks: + emly: + driver: bridge + ipam: + config: + - subnet: 172.16.32.0/24 + gateway: 172.16.32.1 diff --git a/server/docker-compose.yml b/server/compose-prod.yml similarity index 95% rename from server/docker-compose.yml rename to server/compose-prod.yml index ffb3229..0bff7c9 100644 --- a/server/docker-compose.yml +++ b/server/compose-prod.yml @@ -33,6 +33,8 @@ services: PORT: 3000 RATE_LIMIT_MAX: ${RATE_LIMIT_MAX:-5} RATE_LIMIT_WINDOW_HOURS: ${RATE_LIMIT_WINDOW_HOURS:-24} + volumes: + - ./logs/api:/app/logs restart: on-failure depends_on: mysql: @@ -49,10 +51,11 @@ services: MYSQL_USER: ${MYSQL_USER:-emly} MYSQL_PASSWORD: ${MYSQL_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE:-emly_bugreports} + volumes: + - ./logs/dashboard:/app/logs depends_on: mysql: condition: service_healthy - networks: emly: ipv4_address: 172.16.32.4 diff --git a/server/dashboard/src/hooks.server.ts b/server/dashboard/src/hooks.server.ts index 773bb01..0909941 100644 --- a/server/dashboard/src/hooks.server.ts +++ b/server/dashboard/src/hooks.server.ts @@ -1,7 +1,18 @@ import type { Handle } from '@sveltejs/kit'; import { lucia } from '$lib/server/auth'; +import { initLogger, Log } from '$lib/server/logger'; + +// Initialize dashboard logger +initLogger(); export const handle: Handle = async ({ event, resolve }) => { + const ip = + event.request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || + event.request.headers.get('x-real-ip') || + event.getClientAddress?.() || + 'unknown'; + Log('HTTP', `${event.request.method} ${event.url.pathname} from ${ip}`); + const sessionId = event.cookies.get(lucia.sessionCookieName); if (!sessionId) { @@ -21,6 +32,7 @@ export const handle: Handle = async ({ event, resolve }) => { } if (!session) { + Log('AUTH', `Invalid session from ip=${ip}`); const sessionCookie = lucia.createBlankSessionCookie(); event.cookies.set(sessionCookie.name, sessionCookie.value, { path: '.', @@ -30,6 +42,7 @@ export const handle: Handle = async ({ event, resolve }) => { // If user is disabled, invalidate their session and clear cookie if (session && user && !user.enabled) { + Log('AUTH', `Disabled user rejected: username=${user.username} ip=${ip}`); await lucia.invalidateSession(session.id); const sessionCookie = lucia.createBlankSessionCookie(); event.cookies.set(sessionCookie.name, sessionCookie.value, { diff --git a/server/dashboard/src/lib/server/logger.ts b/server/dashboard/src/lib/server/logger.ts new file mode 100644 index 0000000..d2769d1 --- /dev/null +++ b/server/dashboard/src/lib/server/logger.ts @@ -0,0 +1,42 @@ +import { mkdirSync, appendFileSync, existsSync } from "fs"; +import { join } from "path"; + +let logFilePath: string | null = null; + +/** + * Initialize the logger. Creates the logs/ directory if needed + * and opens the log file in append mode. + */ +export function initLogger(filename = "dashboard.log"): void { + const logsDir = join(process.cwd(), "logs"); + if (!existsSync(logsDir)) { + mkdirSync(logsDir, { recursive: true }); + } + logFilePath = join(logsDir, filename); + Log("LOGGER", "Logger initialized. Writing to:", logFilePath); +} + +/** + * Log a timestamped, source-tagged message to stdout and the log file. + * Format: [YYYY-MM-DD] - [HH:MM:SS] - [source] - message + */ +export function Log(source: string, ...args: unknown[]): void { + const now = new Date(); + const date = now.toISOString().slice(0, 10); + const time = now.toTimeString().slice(0, 8); + const msg = args + .map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))) + .join(" "); + + const line = `[${date}] - [${time}] - [${source}] - ${msg}`; + + console.log(line); + + if (logFilePath) { + try { + appendFileSync(logFilePath, line + "\n"); + } catch { + // If file write fails, stdout logging still works + } + } +} diff --git a/server/src/db/migrate.ts b/server/src/db/migrate.ts index 2d0d80c..28ce54d 100644 --- a/server/src/db/migrate.ts +++ b/server/src/db/migrate.ts @@ -3,6 +3,7 @@ import { join } from "path"; import { randomUUID } from "crypto"; import { hash } from "@node-rs/argon2"; import { getPool } from "./connection"; +import { Log } from "../logger"; export async function runMigrations(): Promise { const pool = getPool(); @@ -51,8 +52,8 @@ export async function runMigrations(): Promise { "INSERT INTO `user` (`id`, `username`, `password_hash`, `role`) VALUES (?, ?, ?, ?)", [id, "admin", passwordHash, "admin"] ); - console.log("Default admin user created (username: admin, password: admin)"); + Log("MIGRATE", "Default admin user created (username: admin, password: admin)"); } - console.log("Database migrations completed"); + Log("MIGRATE", "Database migrations completed"); } diff --git a/server/src/index.ts b/server/src/index.ts index d6e9887..cddf314 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -4,6 +4,10 @@ import { runMigrations } from "./db/migrate"; import { closePool } from "./db/connection"; import { bugReportRoutes } from "./routes/bugReports"; import { adminRoutes } from "./routes/admin"; +import { initLogger, Log } from "./logger"; + +// Initialize logger +initLogger(); // Validate environment validateConfig(); @@ -12,8 +16,20 @@ validateConfig(); await runMigrations(); const app = new Elysia() + .onRequest(({ request }) => { + const url = new URL(request.url); + const ip = + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || + request.headers.get("x-real-ip") || + "unknown"; + Log("HTTP", `${request.method} ${url.pathname} from ${ip}`); + }) + .onAfterResponse(({ request, set }) => { + const url = new URL(request.url); + Log("HTTP", `${request.method} ${url.pathname} -> ${set.status ?? 200}`); + }) .onError(({ error, set }) => { - console.error("Unhandled error:", error); + Log("ERROR", "Unhandled error:", error); set.status = 500; return { success: false, message: "Internal server error" }; }) @@ -25,19 +41,20 @@ const app = new Elysia() maxBody: 50 * 1024 * 1024, // 50MB }); -console.log( +Log( + "SERVER", `EMLy Bug Report API running on http://localhost:${app.server?.port}` ); // Graceful shutdown process.on("SIGINT", async () => { - console.log("Shutting down..."); + Log("SERVER", "Shutting down (SIGINT)..."); await closePool(); process.exit(0); }); process.on("SIGTERM", async () => { - console.log("Shutting down..."); + Log("SERVER", "Shutting down (SIGTERM)..."); await closePool(); process.exit(0); }); diff --git a/server/src/logger.ts b/server/src/logger.ts new file mode 100644 index 0000000..bc953a4 --- /dev/null +++ b/server/src/logger.ts @@ -0,0 +1,42 @@ +import { mkdirSync, appendFileSync, existsSync } from "fs"; +import { join } from "path"; + +let logFilePath: string | null = null; + +/** + * Initialize the logger. Creates the logs/ directory if needed + * and opens the log file in append mode. + */ +export function initLogger(filename = "api.log"): void { + const logsDir = join(process.cwd(), "logs"); + if (!existsSync(logsDir)) { + mkdirSync(logsDir, { recursive: true }); + } + logFilePath = join(logsDir, filename); + Log("LOGGER", "Logger initialized. Writing to:", logFilePath); +} + +/** + * Log a timestamped, source-tagged message to stdout and the log file. + * Format: [YYYY-MM-DD] - [HH:MM:SS] - [source] - message + */ +export function Log(source: string, ...args: unknown[]): void { + const now = new Date(); + const date = now.toISOString().slice(0, 10); + const time = now.toTimeString().slice(0, 8); + const msg = args + .map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))) + .join(" "); + + const line = `[${date}] - [${time}] - [${source}] - ${msg}`; + + console.log(line); + + if (logFilePath) { + try { + appendFileSync(logFilePath, line + "\n"); + } catch { + // If file write fails, stdout logging still works + } + } +} diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index 6c084b7..df1e2b6 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -1,11 +1,17 @@ import { Elysia } from "elysia"; import { config } from "../config"; +import { Log } from "../logger"; export const apiKeyGuard = new Elysia({ name: "api-key-guard" }).derive( { as: "scoped" }, - ({ headers, error }) => { + ({ headers, error, request }) => { const key = headers["x-api-key"]; if (!key || key !== config.apiKey) { + const ip = + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || + request.headers.get("x-real-ip") || + "unknown"; + Log("AUTH", `Invalid API key from ip=${ip}`); return error(401, { success: false, message: "Invalid or missing API key" }); } return {}; @@ -14,9 +20,14 @@ export const apiKeyGuard = new Elysia({ name: "api-key-guard" }).derive( export const adminKeyGuard = new Elysia({ name: "admin-key-guard" }).derive( { as: "scoped" }, - ({ headers, error }) => { + ({ headers, error, request }) => { const key = headers["x-admin-key"]; if (!key || key !== config.adminKey) { + const ip = + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || + request.headers.get("x-real-ip") || + "unknown"; + Log("AUTH", `Invalid admin key from ip=${ip}`); return error(401, { success: false, message: "Invalid or missing admin key" }); } return {}; diff --git a/server/src/middleware/rateLimit.ts b/server/src/middleware/rateLimit.ts index dfd3ded..70921d8 100644 --- a/server/src/middleware/rateLimit.ts +++ b/server/src/middleware/rateLimit.ts @@ -1,6 +1,7 @@ import { Elysia } from "elysia"; import { getPool } from "../db/connection"; import { config } from "../config"; +import { Log } from "../logger"; const excludedHwids = new Set([ // Add HWIDs here for development testing @@ -54,6 +55,7 @@ export const hwidRateLimit = new Elysia({ name: "hwid-rate-limit" }).derive( if (entry.count >= config.rateLimit.max) { const retryAfterMs = windowMs - elapsed; const retryAfterMin = Math.ceil(retryAfterMs / 60000); + Log("RATELIMIT", `Rate limit hit hwid=${hwid} count=${entry.count}`); return error(429, { success: false, message: `Rate limit exceeded. Try again in ${retryAfterMin} minutes.`, diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index de42a98..3bc3e72 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -7,6 +7,7 @@ import { deleteBugReport, updateBugReportStatus, } from "../services/bugReportService"; +import { Log } from "../logger"; import type { BugReportStatus } from "../types"; export const adminRoutes = new Elysia({ prefix: "/api/admin" }) @@ -18,6 +19,7 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) const pageSize = Math.min(parseInt(query.pageSize || "20"), 100); const status = query.status as BugReportStatus | undefined; + Log("ADMIN", `List bug reports page=${page} pageSize=${pageSize} status=${status || "all"}`); return await listBugReports({ page, pageSize, status }); }, { @@ -39,6 +41,7 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) .get( "/bug-reports/:id", async ({ params, error }) => { + Log("ADMIN", `Get bug report id=${params.id}`); const result = await getBugReport(parseInt(params.id)); if (!result) return error(404, { success: false, message: "Report not found" }); return result; @@ -51,6 +54,7 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) .patch( "/bug-reports/:id/status", async ({ params, body, error }) => { + Log("ADMIN", `Update status id=${params.id} status=${body.status}`); const updated = await updateBugReportStatus( parseInt(params.id), body.status @@ -92,6 +96,7 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) .delete( "/bug-reports/:id", async ({ params, error }) => { + Log("ADMIN", `Delete bug report id=${params.id}`); const deleted = await deleteBugReport(parseInt(params.id)); if (!deleted) return error(404, { success: false, message: "Report not found" }); diff --git a/server/src/routes/bugReports.ts b/server/src/routes/bugReports.ts index 8541705..21bc61f 100644 --- a/server/src/routes/bugReports.ts +++ b/server/src/routes/bugReports.ts @@ -2,6 +2,7 @@ import { Elysia, t } from "elysia"; import { apiKeyGuard } from "../middleware/auth"; import { hwidRateLimit } from "../middleware/rateLimit"; import { createBugReport, addFile } from "../services/bugReportService"; +import { Log } from "../logger"; import type { FileRole } from "../types"; const FILE_ROLES: { field: string; role: FileRole; mime: string }[] = [ @@ -19,6 +20,14 @@ export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" }) async ({ body, request, set }) => { const { name, email, description, hwid, hostname, os_user, system_info } = body; + // Get submitter IP from headers or connection + const submitterIp = + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || + request.headers.get("x-real-ip") || + "unknown"; + + Log("BUGREPORT", `Received from name=${name} hwid=${hwid || "none"} ip=${submitterIp}`); + // Parse system_info — may arrive as a JSON string or already-parsed object let systemInfo: Record | null = null; if (system_info) { @@ -33,12 +42,6 @@ export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" }) } } - // Get submitter IP from headers or connection - const submitterIp = - request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || - request.headers.get("x-real-ip") || - "unknown"; - // Create the bug report const reportId = await createBugReport({ name, @@ -56,6 +59,7 @@ export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" }) const file = body[field as keyof typeof body]; if (file && file instanceof File) { const buffer = Buffer.from(await file.arrayBuffer()); + Log("BUGREPORT", `File uploaded: role=${role} size=${buffer.length} bytes`); await addFile({ report_id: reportId, file_role: role, @@ -67,6 +71,8 @@ export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" }) } } + Log("BUGREPORT", `Created successfully with id=${reportId}`); + set.status = 201; return { success: true,