Files
HealthLog/src/app/api/admin/status/route.ts
T
Marc-André Bombeck 3c3e5ceab7 fix(config): treat an empty environment variable as unset
docker-compose.yml forwards its whitelist as "${NAME:-}", so a variable
the operator never set reaches the process as the empty string. Reads
written as a nullish fallback kept that string: the weather client built
an empty base URL and every geocode failed with ERR_INVALID_URL, and the
IP geo lookup requested a relative path.

Reads with a fallback now go through src/lib/env.ts (envValue, envOr,
envFlag), which treats empty and whitespace as unset. IP_GEO_LOOKUP_DISABLED
accepts 1, true, yes and on instead of only "1". A structural test refuses
a ?? fallback written directly against process.env anywhere in src/, and
the compose whitelist guard counts the helper's call shape as a read.
2026-09-26 15:20:42 +02:00

114 lines
3.8 KiB
TypeScript

import { envOr } from "@/lib/env";
import { prisma } from "@/lib/db";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
import { apiSuccess } from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
import { getWorkerStatus } from "@/lib/jobs/worker-status";
import {
JOB_FAILURE_WINDOW_HOURS,
readFailingQueues,
} from "@/lib/jobs/job-failures";
import { readFileSync } from "node:fs";
// Build timestamp (set at build time via next.config)
const BUILD_TIME = process.env.BUILD_TIMESTAMP || new Date().toISOString();
const START_TIME = new Date().toISOString();
// Try to read git commit info
let gitCommit = envOr("GIT_COMMIT", "unknown");
try {
const head = readFileSync(".git/HEAD", "utf-8").trim();
if (head.startsWith("ref:")) {
const ref = head.replace("ref: ", "");
gitCommit = readFileSync(`.git/${ref}`, "utf-8").trim().slice(0, 8);
} else {
gitCommit = head.slice(0, 8);
}
} catch {
// In Docker or production, .git may not exist
}
export const GET = apiHandler(async () => {
await requireAdmin();
annotate({ action: { name: "admin.status" } });
const [
userCount,
measurementCount,
medicationCount,
intakeEventCount,
tokenCount,
sessionCount,
appSettings,
] = await Promise.all([
prisma.user.count(),
// v1.4.41 W-DELETED-2 — exclude soft-deleted measurements from the
// admin status count so the dashboard tile reflects live data only.
prisma.measurement.count({ where: { deletedAt: null } }),
prisma.medication.count(),
prisma.medicationIntakeEvent.count(),
prisma.apiToken.count({ where: { revoked: false } }),
prisma.session.count(),
prisma.appSettings.findUnique({ where: { id: "singleton" } }),
]);
const umamiConfigured = Boolean(
appSettings?.umamiScriptUrl && appSettings?.umamiWebsiteId,
);
const glitchtipConfigured = Boolean(appSettings?.glitchtipDsn);
const webPushConfigured = Boolean(
appSettings?.webPushVapidPublicKey &&
appSettings?.webPushVapidPrivateKeyEncrypted &&
appSettings?.webPushVapidSubject,
);
const workerStatus = getWorkerStatus();
// `worker.errors` is one unnamed counter, which is how a queue that failed on
// every run for months could sit behind a number nobody could act on. The
// queue itself knows which job broke and why, so that is what the status
// reports. `null` = no queue schema to ask (web-only deployment); an empty
// array = asked, nothing is failing.
const failingJobs = await readFailingQueues();
return apiSuccess({
version: process.env.npm_package_version || "0.1.0",
nodeVersion: process.version,
gitCommit,
buildTime: BUILD_TIME,
startTime: START_TIME,
database: "connected",
worker: {
running: workerStatus.running,
startedAt: workerStatus.startedAt,
lastHeartbeat: workerStatus.lastHeartbeat,
lastReminderCheck: workerStatus.lastReminderCheck,
lastWithingsSync: workerStatus.lastWithingsSync,
lastInsightsRun: workerStatus.lastInsightsRun,
jobsProcessed: workerStatus.jobsProcessed,
},
failingJobs:
failingJobs === null
? null
: { windowHours: JOB_FAILURE_WINDOW_HOURS, queues: failingJobs },
counts: {
users: userCount,
measurements: measurementCount,
medications: medicationCount,
intakeEvents: intakeEventCount,
activeTokens: tokenCount,
activeSessions: sessionCount,
},
integrations: {
umami: umamiConfigured
? { configured: true, enabled: appSettings?.umamiEnabled ?? false }
: null,
glitchtip: glitchtipConfigured
? {
configured: true,
enabled: appSettings?.glitchtipEnabled ?? false,
}
: null,
webPush: webPushConfigured ? { configured: true } : null,
},
});
});