Implement Monlet MVP stack and UI updates
This commit is contained in:
648
web/src/lib/api-types.ts
Normal file
648
web/src/lib/api-types.ts
Normal file
@@ -0,0 +1,648 @@
|
||||
/**
|
||||
* This file was auto-generated by openapi-typescript.
|
||||
* Do not make direct changes to the file.
|
||||
*/
|
||||
|
||||
export interface paths {
|
||||
"/api/v1/health": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Health check */
|
||||
get: operations["getHealth"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/ready": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Readiness check */
|
||||
get: operations["getReady"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/heartbeat": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Ingest agent heartbeat */
|
||||
post: operations["ingestHeartbeat"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/events": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Ingest agent check result events */
|
||||
post: operations["ingestEvents"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/agents": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** List agents */
|
||||
get: operations["listAgents"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/agents/{agent_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get agent detail */
|
||||
get: operations["getAgent"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/checks": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** List current check states */
|
||||
get: operations["listChecks"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/incidents": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** List incidents */
|
||||
get: operations["listIncidents"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/events/query": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Query stored events */
|
||||
get: operations["queryEvents"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/notifiers/outbox": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** List notification outbox items */
|
||||
get: operations["listOutbox"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
HealthResponse: {
|
||||
/** @enum {string} */
|
||||
status: "ok";
|
||||
};
|
||||
ErrorResponse: {
|
||||
error: {
|
||||
/** @enum {string} */
|
||||
code: "unauthorized" | "not_found" | "validation" | "payload_too_large" | "rate_limited" | "internal" | "not_ready";
|
||||
message: string;
|
||||
/** @description Server-generated request id, mirrored from X-Request-Id. */
|
||||
request_id: string;
|
||||
};
|
||||
};
|
||||
PageEnvelope: {
|
||||
/** @description Pass back as `cursor` to fetch the next page. Null/absent if no more results. */
|
||||
next_cursor?: string | null;
|
||||
};
|
||||
AcceptedResponse: {
|
||||
accepted: boolean;
|
||||
};
|
||||
EventBatchAcceptedResponse: {
|
||||
/** @description Number of events stored on this request. */
|
||||
accepted: number;
|
||||
/** @description Number of events whose `event_id` was already known. */
|
||||
deduplicated: number;
|
||||
};
|
||||
HeartbeatRequest: {
|
||||
agent_id: string;
|
||||
/** Format: date-time */
|
||||
observed_at: string;
|
||||
hostname: string;
|
||||
features: components["schemas"]["AgentFeatures"];
|
||||
/** @description Up to 32 labels. Key max 64, value max 256. Monlet agents reserve `monlet_agent_version`. Secret-like keys are rejected. */
|
||||
labels?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
AgentFeatures: {
|
||||
/** @description Agent pushes heartbeats/events to the server. */
|
||||
push: boolean;
|
||||
/** @description Agent exposes a Prometheus-compatible metrics endpoint. */
|
||||
metrics: boolean;
|
||||
};
|
||||
EventBatchRequest: {
|
||||
agent_id: string;
|
||||
events: components["schemas"]["CheckResultEvent"][];
|
||||
};
|
||||
/**
|
||||
* @description Event body used inside `EventBatchRequest.events`. The owning `agent_id`
|
||||
* is supplied once at the batch envelope; per-event `agent_id` is intentionally
|
||||
* absent so requests cannot mix or spoof other agents under a shared token.
|
||||
* Server response endpoints (`GET /events/query`) include `agent_id` separately
|
||||
* in their item schema.
|
||||
*/
|
||||
CheckResultEvent: {
|
||||
/** @description UUIDv7 string (lower-case, with dashes). See ADR-0006. */
|
||||
event_id: string;
|
||||
check_id: string;
|
||||
/** Format: date-time */
|
||||
observed_at: string;
|
||||
/** @enum {string} */
|
||||
status: "ok" | "warning" | "critical" | "unknown";
|
||||
exit_code: number;
|
||||
duration_ms: number;
|
||||
/**
|
||||
* @description Limit is 8 KiB measured in UTF-8 **bytes** (not characters). Agent truncates
|
||||
* on a UTF-8 boundary before send and appends marker `...[truncated N bytes]`.
|
||||
* `maxLength: 8192` here is a coarse schema-level upper bound; servers MUST
|
||||
* additionally reject events whose UTF-8 byte length exceeds 8192 with
|
||||
* `400 validation`.
|
||||
*/
|
||||
output?: string;
|
||||
/** @default false */
|
||||
output_truncated: boolean;
|
||||
/**
|
||||
* @description If false, the server stores state/incidents but does not enqueue notifications for this event.
|
||||
* @default true
|
||||
*/
|
||||
notifications_enabled: boolean;
|
||||
incident_key?: string;
|
||||
};
|
||||
/** @description Stored event returned by `GET /events/query`. Includes the resolved `agent_id` and server-side `received_at`. */
|
||||
StoredCheckResultEvent: components["schemas"]["CheckResultEvent"] & {
|
||||
agent_id: string;
|
||||
/** Format: date-time */
|
||||
received_at: string;
|
||||
};
|
||||
Agent: {
|
||||
agent_id: string;
|
||||
hostname: string;
|
||||
/** @enum {string} */
|
||||
status: "alive" | "stale" | "dead";
|
||||
/** Format: date-time */
|
||||
last_seen_at: string;
|
||||
features: components["schemas"]["AgentFeatures"];
|
||||
labels?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
CheckState: {
|
||||
agent_id: string;
|
||||
check_id: string;
|
||||
/** @enum {string} */
|
||||
status: "ok" | "warning" | "critical" | "unknown";
|
||||
/** Format: date-time */
|
||||
last_observed_at: string;
|
||||
exit_code?: number;
|
||||
incident_key?: string;
|
||||
};
|
||||
Incident: {
|
||||
id: string;
|
||||
incident_key: string;
|
||||
/** @enum {string} */
|
||||
state: "open" | "resolved";
|
||||
/** @enum {string} */
|
||||
severity: "warning" | "critical" | "unknown";
|
||||
agent_id?: string;
|
||||
check_id?: string;
|
||||
/** Format: date-time */
|
||||
opened_at: string;
|
||||
/** Format: date-time */
|
||||
resolved_at?: string | null;
|
||||
summary?: string;
|
||||
};
|
||||
NotificationOutboxItem: {
|
||||
id: string;
|
||||
notifier: string;
|
||||
/** @enum {string} */
|
||||
state: "pending" | "sending" | "sent" | "retry" | "failed" | "discarded";
|
||||
incident_id: string;
|
||||
/** @enum {string} */
|
||||
event_type: "firing" | "resolved";
|
||||
attempts: number;
|
||||
/** Format: date-time */
|
||||
next_attempt_at?: string | null;
|
||||
last_error?: string | null;
|
||||
/** Format: date-time */
|
||||
updated_at?: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Authentication failed. */
|
||||
Unauthorized: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Resource not found. */
|
||||
NotFound: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Payload exceeds configured limit (1 MiB). */
|
||||
PayloadTooLarge: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Request body or parameters failed validation. */
|
||||
ValidationError: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unexpected server error. */
|
||||
InternalError: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
parameters: {
|
||||
AgentId: string;
|
||||
/** @description Opaque pagination cursor returned in `next_cursor`. */
|
||||
Cursor: string;
|
||||
Limit: number;
|
||||
};
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export interface operations {
|
||||
getHealth: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Service is alive. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HealthResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
getReady: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Service is ready. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HealthResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Service is not ready. */
|
||||
503: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
ingestHeartbeat: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HeartbeatRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Heartbeat accepted. */
|
||||
202: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AcceptedResponse"];
|
||||
};
|
||||
};
|
||||
400: components["responses"]["ValidationError"];
|
||||
401: components["responses"]["Unauthorized"];
|
||||
413: components["responses"]["PayloadTooLarge"];
|
||||
500: components["responses"]["InternalError"];
|
||||
};
|
||||
};
|
||||
ingestEvents: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["EventBatchRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Events accepted. */
|
||||
202: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["EventBatchAcceptedResponse"];
|
||||
};
|
||||
};
|
||||
400: components["responses"]["ValidationError"];
|
||||
401: components["responses"]["Unauthorized"];
|
||||
413: components["responses"]["PayloadTooLarge"];
|
||||
500: components["responses"]["InternalError"];
|
||||
};
|
||||
};
|
||||
listAgents: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Opaque pagination cursor returned in `next_cursor`. */
|
||||
cursor?: components["parameters"]["Cursor"];
|
||||
limit?: components["parameters"]["Limit"];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Agent list. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PageEnvelope"] & {
|
||||
items: components["schemas"]["Agent"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
401: components["responses"]["Unauthorized"];
|
||||
};
|
||||
};
|
||||
getAgent: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
agent_id: components["parameters"]["AgentId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Agent detail. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Agent"];
|
||||
};
|
||||
};
|
||||
401: components["responses"]["Unauthorized"];
|
||||
404: components["responses"]["NotFound"];
|
||||
};
|
||||
};
|
||||
listChecks: {
|
||||
parameters: {
|
||||
query?: {
|
||||
agent_id?: string;
|
||||
/** @description Opaque pagination cursor returned in `next_cursor`. */
|
||||
cursor?: components["parameters"]["Cursor"];
|
||||
limit?: components["parameters"]["Limit"];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Check state list. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PageEnvelope"] & {
|
||||
items: components["schemas"]["CheckState"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
401: components["responses"]["Unauthorized"];
|
||||
};
|
||||
};
|
||||
listIncidents: {
|
||||
parameters: {
|
||||
query?: {
|
||||
state?: "open" | "resolved";
|
||||
/** @description Opaque pagination cursor returned in `next_cursor`. */
|
||||
cursor?: components["parameters"]["Cursor"];
|
||||
limit?: components["parameters"]["Limit"];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Incident list. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PageEnvelope"] & {
|
||||
items: components["schemas"]["Incident"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
401: components["responses"]["Unauthorized"];
|
||||
};
|
||||
};
|
||||
queryEvents: {
|
||||
parameters: {
|
||||
query?: {
|
||||
agent_id?: string;
|
||||
check_id?: string;
|
||||
/** @description Opaque pagination cursor returned in `next_cursor`. */
|
||||
cursor?: components["parameters"]["Cursor"];
|
||||
limit?: components["parameters"]["Limit"];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Event list. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PageEnvelope"] & {
|
||||
items: components["schemas"]["StoredCheckResultEvent"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
401: components["responses"]["Unauthorized"];
|
||||
};
|
||||
};
|
||||
listOutbox: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Opaque pagination cursor returned in `next_cursor`. */
|
||||
cursor?: components["parameters"]["Cursor"];
|
||||
limit?: components["parameters"]["Limit"];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Outbox list. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PageEnvelope"] & {
|
||||
items: components["schemas"]["NotificationOutboxItem"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
401: components["responses"]["Unauthorized"];
|
||||
};
|
||||
};
|
||||
}
|
||||
67
web/src/lib/api.ts
Normal file
67
web/src/lib/api.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import "server-only";
|
||||
|
||||
import type { components, operations } from "./api-types";
|
||||
|
||||
export type Agent = components["schemas"]["Agent"];
|
||||
export type CheckState = components["schemas"]["CheckState"];
|
||||
export type Incident = components["schemas"]["Incident"];
|
||||
export type StoredEvent = components["schemas"]["StoredCheckResultEvent"];
|
||||
export type OutboxItem = components["schemas"]["NotificationOutboxItem"];
|
||||
|
||||
type OkBody<O> = O extends { responses: { 200: { content: { "application/json": infer B } } } }
|
||||
? B
|
||||
: never;
|
||||
type QueryOf<O> = O extends { parameters: { query?: infer Q } } ? NonNullable<Q> : never;
|
||||
|
||||
type AgentsBody = OkBody<operations["listAgents"]>;
|
||||
type ChecksBody = OkBody<operations["listChecks"]>;
|
||||
type IncidentsBody = OkBody<operations["listIncidents"]>;
|
||||
type EventsBody = OkBody<operations["queryEvents"]>;
|
||||
type OutboxBody = OkBody<operations["listOutbox"]>;
|
||||
|
||||
const BASE = process.env.MONLET_API_BASE_URL ?? "http://127.0.0.1:8000";
|
||||
const TOKEN = process.env.MONLET_API_TOKEN ?? "";
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(public status: number, public code: string, message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T> {
|
||||
const url = new URL(path, BASE);
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v !== undefined && v !== "") url.searchParams.set(k, String(v));
|
||||
}
|
||||
}
|
||||
const headers: Record<string, string> = { Accept: "application/json" };
|
||||
if (TOKEN) headers.Authorization = `Bearer ${TOKEN}`;
|
||||
const res = await fetch(url, { headers, cache: "no-store" });
|
||||
if (!res.ok) {
|
||||
let code = "http_error";
|
||||
let message = res.statusText;
|
||||
try {
|
||||
const body = await res.json();
|
||||
code = body?.error?.code ?? code;
|
||||
message = body?.error?.message ?? message;
|
||||
} catch {}
|
||||
throw new ApiError(res.status, code, message);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
listAgents: (q: QueryOf<operations["listAgents"]> = {}) =>
|
||||
get<AgentsBody>("/api/v1/agents", q),
|
||||
getAgent: (agentId: string) => get<Agent>(`/api/v1/agents/${encodeURIComponent(agentId)}`),
|
||||
listChecks: (q: QueryOf<operations["listChecks"]> = {}) =>
|
||||
get<ChecksBody>("/api/v1/checks", q),
|
||||
listIncidents: (q: QueryOf<operations["listIncidents"]> = {}) =>
|
||||
get<IncidentsBody>("/api/v1/incidents", q),
|
||||
queryEvents: (q: QueryOf<operations["queryEvents"]> = {}) =>
|
||||
get<EventsBody>("/api/v1/events/query", q),
|
||||
listOutbox: (q: QueryOf<operations["listOutbox"]> = {}) =>
|
||||
get<OutboxBody>("/api/v1/notifiers/outbox", q),
|
||||
health: () => get<{ status: string }>("/api/v1/health"),
|
||||
};
|
||||
28
web/src/lib/format.ts
Normal file
28
web/src/lib/format.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export function fmtDate(iso?: string | null): string {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
return Number.isNaN(d.getTime()) ? iso : d.toISOString().replace("T", " ").replace(/\..+$/, "Z");
|
||||
}
|
||||
|
||||
export function statusBadgeClass(s: string): string {
|
||||
switch (s) {
|
||||
case "ok":
|
||||
case "alive":
|
||||
case "resolved":
|
||||
case "sent":
|
||||
return "text-emerald-400";
|
||||
case "warning":
|
||||
case "stale":
|
||||
case "retry":
|
||||
return "text-amber-400";
|
||||
case "critical":
|
||||
case "dead":
|
||||
case "open":
|
||||
case "failed":
|
||||
return "text-red-400";
|
||||
case "discarded":
|
||||
return "text-neutral-500";
|
||||
default:
|
||||
return "text-neutral-300";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user