feat: implement dashboard with authentication and event logging
- Added dashboard functionality at /status with Basic Auth using DASHBOARD_PASSWORD. - Created server-side event logging to track catalog and university status. - Implemented traffic metrics tracking and display on the dashboard. - Enhanced API tests to cover new dashboard features and authentication. - Introduced CSS and JS for dashboard styling and functionality. - Added tests for dashboard access control and data retrieval.
This commit is contained in:
@@ -14,6 +14,8 @@ import { securityHeaders, requestTimeout } from "./protect.js";
|
||||
import { startWatch, watchOnce, watchStatus } from "./watch.js";
|
||||
import { telegram } from "./alerts.js";
|
||||
import { requestLog, trafficStatus } from "./metrics.js";
|
||||
import { dashboardAuth } from "./auth.js";
|
||||
import { listEvents } from "./events.js";
|
||||
import {
|
||||
getIndex,
|
||||
indexStatus,
|
||||
@@ -75,8 +77,9 @@ app.use("/api", (_req, res, next) => {
|
||||
// 9 — a request limit on /api (429 too_many_requests), a 30 s time limit (504 timeout), security headers;
|
||||
// 10 — parserVersion in /api/catalog;
|
||||
// 11 — the catalog is checked by ETag every 5 minutes; `watch`, `telegram` and `catalog.lastCrawl` in /api/health;
|
||||
// 12 — a JSON log line per request and `traffic` in /api/health.
|
||||
const API_VERSION = 12;
|
||||
// 12 — a JSON log line per request;
|
||||
// 13 — the dashboard at /status (password), /api/health only says alive, ready and versions.
|
||||
const API_VERSION = 13;
|
||||
|
||||
/**
|
||||
* What the server still waits for before it answers from memory: the catalog, the week table
|
||||
@@ -85,25 +88,55 @@ const API_VERSION = 12;
|
||||
const waitingFor = (sources, directory) =>
|
||||
[!sources.catalog && "catalog", !sources.calendar && "calendar", !directory.builtAt && "index"].filter(Boolean);
|
||||
// Liveness plus what the server currently reads from the university, without network calls.
|
||||
// Public: alive, ready, versions. Everything else about the server is behind the dashboard's
|
||||
// password (/status), as it names documents, clients and the owner's set-up.
|
||||
app.get("/api/health", async (req, res) => {
|
||||
const sources = await sourceStatus().catch((e) => ({ error: e.message }));
|
||||
const sources = await sourceStatus().catch(() => ({}));
|
||||
const waiting = waitingFor(sources, indexStatus());
|
||||
res.json({
|
||||
ok: true,
|
||||
// The caller's address as the server sees it: behind a proxy it must be the client's own,
|
||||
// or every visitor shares one request limit.
|
||||
client: req.ip,
|
||||
ready: waitingFor(sources, indexStatus()).length === 0,
|
||||
ready: waiting.length === 0,
|
||||
waiting,
|
||||
apiVersion: API_VERSION,
|
||||
parserVersion: PARSER_VERSION,
|
||||
...sources,
|
||||
directory: indexStatus(),
|
||||
// The server's own checks of the catalog and its messages to the owner.
|
||||
watch: watchStatus,
|
||||
// What the API was asked in the last hour and since the start (server/metrics.js).
|
||||
traffic: trafficStatus(),
|
||||
telegram: { enabled: telegram.enabled, sent: telegram.sent, lastSentAt: telegram.lastSentAt, lastError: telegram.lastError },
|
||||
year: sources.catalog?.year || null,
|
||||
});
|
||||
});
|
||||
const startedAt = new Date().toISOString();
|
||||
// The dashboard (server/status/) and its data, behind DASHBOARD_PASSWORD. The data sits under
|
||||
// /status too, so the browser sends the password it was given for the page.
|
||||
const statusPage = fileURLToPath(new URL("./status/", import.meta.url));
|
||||
app.use("/status", dashboardAuth());
|
||||
app.get("/status/data.json", async (_req, res, next) => {
|
||||
try {
|
||||
const sources = await sourceStatus();
|
||||
const directory = indexStatus();
|
||||
const idx = readyIndex();
|
||||
res.json({
|
||||
server: {
|
||||
apiVersion: API_VERSION,
|
||||
parserVersion: PARSER_VERSION,
|
||||
startedAt,
|
||||
node: process.version,
|
||||
ready: waitingFor(sources, directory).length === 0,
|
||||
waiting: waitingFor(sources, directory),
|
||||
},
|
||||
...sources,
|
||||
directory,
|
||||
watch: watchStatus,
|
||||
telegram: { enabled: telegram.enabled, sent: telegram.sent, lastSentAt: telegram.lastSentAt, lastError: telegram.lastError },
|
||||
traffic: trafficStatus(),
|
||||
events: await listEvents(50),
|
||||
diagnostics: idx && directory.builtAt ? catalogDiagnostics(idx) : null,
|
||||
});
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.use("/status", express.static(statusPage, { maxAge: 0 }));
|
||||
// Readiness for Docker and a proxy: 503 until the catalog, the week table and the index are
|
||||
// loaded, so traffic does not reach a server whose first requests would wait half a minute.
|
||||
app.get("/api/ready", async (_req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user