feat(observability): service discovery, health cards, alertmanager widget
Slice 3 of observability-service-registry (frontend). The Observability
page discovers Grafana from the service registry instead of env vars,
adds Grafana + Prometheus health cards, and ships an alertmanager
active_alerts dashboard widget.
- types: added GrafanaStatus + PrometheusStatus; added optional
service_id/error to AlertmanagerStatus.
- api/client.ts + hooks/useObservability.ts: fetchGrafanaStatus,
fetchPrometheusStatus, useGrafanaStatus, usePrometheusStatus.
- widgets/AlertmanagerAlertsWidget.tsx (new): presentational widget
consuming the active_alerts summary shape (total/by_severity/alerts);
exported from widgets/index.ts.
- integrations/registry.ts: alertmanager binding (active_alerts kind,
30s refresh, optional severity_filter); registry.test.ts updated to
6 service types incl alertmanager + a resolve test.
- components/ObservabilityPage.tsx: removed
import.meta.env.VITE_GRAFANA_URL; derive GRAFANA_BASE_URL from the
first enabled grafana service via useServiceInstances("grafana");
added Grafana + Prometheus HealthCards (up/not-configured/unreachable)
with QueryError retry blocks; machine dashboard shows a "No Grafana
service configured" empty-state linking to /services when none is set.
npm run build (tsc -b + vite) clean; 0 lint errors; 72 frontend tests
pass. Reviewed fresh-context (read-only): no blockers.
This commit is contained in:
@@ -9,8 +9,9 @@ import {
|
||||
import type { ServiceInstance, WidgetInstance } from "../types";
|
||||
|
||||
describe("service registry", () => {
|
||||
it("registers the five backend service types", () => {
|
||||
it("registers the backend service types", () => {
|
||||
expect(Object.keys(SERVICE_REGISTRY).sort()).toEqual([
|
||||
"alertmanager",
|
||||
"grafana",
|
||||
"jellyfin",
|
||||
"nextcloud",
|
||||
@@ -23,6 +24,9 @@ describe("service registry", () => {
|
||||
expect(SERVICE_REGISTRY.grafana.widgets.map((w) => w.kind)).toEqual([
|
||||
"link",
|
||||
]);
|
||||
expect(SERVICE_REGISTRY.alertmanager.widgets.map((w) => w.kind)).toEqual([
|
||||
"active_alerts",
|
||||
]);
|
||||
expect(SERVICE_REGISTRY.ssh_tasks.widgets.map((w) => w.kind)).toEqual([
|
||||
"task_output",
|
||||
]);
|
||||
@@ -62,6 +66,35 @@ describe("service registry", () => {
|
||||
expect(resolved?.refreshIntervalMs).toBe(0);
|
||||
});
|
||||
|
||||
it("resolves an alertmanager active_alerts widget", () => {
|
||||
const widget: WidgetInstance = {
|
||||
id: "wam",
|
||||
service_id: "sam",
|
||||
widget_kind: "active_alerts",
|
||||
title: "Alerts",
|
||||
config: {},
|
||||
enabled: true,
|
||||
sort_order: 0,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
};
|
||||
const services: ServiceInstance[] = [
|
||||
{
|
||||
id: "sam",
|
||||
service_type: "alertmanager",
|
||||
name: "Alertmanager",
|
||||
config: { base_url: "http://am:9093" },
|
||||
secrets_set: {},
|
||||
enabled: true,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
},
|
||||
];
|
||||
const resolved = resolveWidget(widget, services);
|
||||
expect(resolved).toBeDefined();
|
||||
expect(resolved?.refreshIntervalMs).toBe(30_000);
|
||||
});
|
||||
|
||||
it("resolves a built-in widget without a service", () => {
|
||||
const widget: WidgetInstance = {
|
||||
id: "w2",
|
||||
|
||||
Reference in New Issue
Block a user