mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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.
114 lines
3.8 KiB
TypeScript
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,
|
|
},
|
|
});
|
|
});
|