Implement Monlet MVP stack and UI updates

This commit is contained in:
Stanislav Rossovskii
2026-05-27 10:01:59 +04:00
parent dcea096327
commit edc51e9c59
145 changed files with 15618 additions and 248 deletions

133
web/tests/mock-server.mjs Normal file
View File

@@ -0,0 +1,133 @@
import { createServer } from "node:http";
const PORT = Number(process.env.MOCK_PORT ?? 8765);
const now = new Date().toISOString();
const fixtures = {
"/api/v1/health": { status: "ok" },
"/api/v1/agents": {
items: [
{
agent_id: "agent-1",
hostname: "host-1",
status: "alive",
last_seen_at: now,
features: { push: true, metrics: false },
labels: { env: "dev", monlet_agent_version: "0.1.0" },
},
{
agent_id: "agent-2",
hostname: "host-2",
status: "stale",
last_seen_at: now,
features: { push: true, metrics: false },
labels: {},
},
],
next_cursor: null,
},
"/api/v1/agents/agent-1": {
agent_id: "agent-1",
hostname: "host-1",
status: "alive",
last_seen_at: now,
features: { push: true, metrics: false },
labels: { env: "dev", monlet_agent_version: "0.1.0" },
},
"/api/v1/checks": {
items: [
{
agent_id: "agent-1",
check_id: "disk",
status: "ok",
exit_code: 0,
last_observed_at: now,
incident_key: "agent-1:disk",
},
{
agent_id: "agent-1",
check_id: "load",
status: "warning",
exit_code: 1,
last_observed_at: now,
incident_key: "agent-1:load",
},
],
next_cursor: null,
},
"/api/v1/incidents": {
items: [
{
id: "00000000-0000-0000-0000-000000000001",
incident_key: "agent-1:load",
state: "open",
severity: "warning",
agent_id: "agent-1",
check_id: "load",
opened_at: now,
summary: "load high",
},
],
next_cursor: null,
},
"/api/v1/events/query": {
items: [
{
event_id: "00000000-0000-7000-8000-000000000001",
agent_id: "agent-1",
check_id: "disk",
observed_at: now,
received_at: now,
status: "ok",
exit_code: 0,
duration_ms: 12,
},
],
next_cursor: null,
},
"/api/v1/notifiers/outbox": {
items: [
{
id: "00000000-0000-0000-0000-000000000002",
notifier: "debug",
state: "sent",
incident_id: "00000000-0000-0000-0000-000000000001",
event_type: "firing",
attempts: 1,
next_attempt_at: null,
last_error: null,
updated_at: now,
},
],
next_cursor: null,
},
};
const server = createServer((req, res) => {
const url = new URL(req.url ?? "/", `http://x`);
let body = fixtures[url.pathname];
if (url.pathname === "/api/v1/checks" && body) {
const agentId = url.searchParams.get("agent_id");
if (agentId) {
body = {
...body,
items: body.items.filter((item) => item.agent_id === agentId),
next_cursor: null,
};
}
}
res.setHeader("Content-Type", "application/json");
if (!body) {
res.statusCode = 404;
res.end(
JSON.stringify({ error: { code: "not_found", message: "not in fixtures", request_id: "mock" } }),
);
return;
}
res.end(JSON.stringify(body));
});
server.listen(PORT, "127.0.0.1", () => {
console.log(`mock server on ${PORT}`);
});

63
web/tests/smoke.spec.ts Normal file
View File

@@ -0,0 +1,63 @@
import { expect, test } from "@playwright/test";
test("overview renders tallies", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading", { name: "Overview" })).toBeVisible();
await expect(page.locator("body")).toContainText("Agents");
await expect(page.locator("body")).toContainText("Checks");
});
test("agents list links to detail", async ({ page }) => {
await page.goto("/agents?sort=status&dir=desc");
await expect(page).toHaveURL(/\/agents$/);
await page.goto("/agents");
await expect(page.getByRole("heading", { name: "Agents" })).toBeVisible();
await expect(page.locator("thead")).not.toContainText("hostname");
await expect(page.locator("thead")).toContainText("features");
await expect(page.locator("thead")).not.toContainText("asc");
await expect(page.locator("thead")).not.toContainText("desc");
await expect(page.locator("thead")).toContainText("↓");
await expect(page.locator("body")).toContainText("checks 2");
await expect(page.locator("body")).toContainText("warn 1");
await page.getByPlaceholder("search agent").fill("host-1");
await expect(page).toHaveURL(/\/agents$/);
await expect(page.locator("tbody")).toContainText("host-1");
await expect(page.locator("tbody")).not.toContainText("host-2");
await page.getByRole("button", { name: "host" }).click();
await expect(page).toHaveURL(/\/agents$/);
await page.getByRole("button", { name: "clear" }).click();
await page.getByRole("link", { name: "agent-1" }).first().click();
await expect(page).toHaveURL(/\/agents\/agent-1$/);
await expect(page.locator("body")).toContainText("hostname");
await expect(page.getByRole("link", { name: "Checks" }).last()).toBeVisible();
await expect(page.locator("tbody").first().locator("tr").first()).toContainText("warning");
await expect(page.locator("thead").first()).not.toContainText("incident_key");
await page.getByRole("link", { name: "check_id" }).click();
await expect(page).toHaveURL(/sort=check_id/);
await page.getByRole("link", { name: "Events" }).last().click();
await expect(page).toHaveURL(/tab=events.*sort=check_id|sort=check_id.*tab=events/);
await expect(page.locator("thead").first()).not.toContainText("exit");
await page.getByRole("link", { name: "Checks" }).last().click();
await expect(page).toHaveURL(/tab=checks.*sort=check_id|sort=check_id.*tab=checks/);
});
test("checks page lists rows", async ({ page }) => {
await page.goto("/checks");
await expect(page.locator("body")).toContainText("disk");
await expect(page.locator("body")).toContainText("load");
});
test("incidents filter renders", async ({ page }) => {
await page.goto("/incidents");
await expect(page.locator("body")).toContainText("load high");
});
test("events page", async ({ page }) => {
await page.goto("/events");
await expect(page.getByRole("heading", { name: "Events" })).toBeVisible();
});
test("outbox page", async ({ page }) => {
await page.goto("/outbox");
await expect(page.locator("body")).toContainText("debug");
});