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:
2026-10-21 00:04:33 +03:00
parent de72d003b8
commit 3ae7b38fca
15 changed files with 1216 additions and 74 deletions

View File

@@ -7,7 +7,8 @@ import { startApi, fixture } from "./support/nextcloud.js";
// rebuilt only for a changed document, and messages to the owner in Telegram (a stand-in here).
let api, telegram, watch;
const messages = [];
const health = async () => (await fetch(api.base + "/api/health")).json();
// What the dashboard shows (/status, behind its password).
const health = async () => (await fetch(api.base + "/status/data.json", { headers: { Authorization: "Basic " + Buffer.from("admin:secret").toString("base64") } })).json();
const lastMessage = () => messages.at(-1)?.text || "";
before(async () => {
@@ -29,6 +30,7 @@ before(async () => {
PSU_ALERT_SOURCE_DOWN_MS: "0",
// Every check reaches the stand-in, however soon after the last one.
PSU_MIN_REFRESH_MS: "0",
DASHBOARD_PASSWORD: "secret",
});
watch = await import("../server/watch.js");
});
@@ -109,5 +111,13 @@ test("Наблюдение: университет не отвечает и сн
await watch.watchOnce();
assert.equal(messages.length, 5);
assert.match(lastMessage(), /снова отвечает/);
assert.equal((await health()).watch.lastError, null);
const { watch: after, events } = await health();
assert.equal(after.lastError, null);
// The dashboard's feed, newest first: everything above, republished-as-is included.
assert.deepEqual(
events.map((e) => e.kind),
["source_up", "source_down", "republished", "republished", "new"],
);
assert.equal(events[2].text, "без новых подозрительных ячеек");
assert.match(events[3].text, /новых подозрительных ячеек: 1/);
});