New "Import from Uptime Kuma" action on the Maintenance page (operator):
pick an Uptime Kuma integration and a duration, and it starts (or
extends) a maintenance window here for every server whose address
matches a monitor Uptime Kuma currently reports as being in maintenance,
reusing the existing monitor-to-server matching from the Uptime Kuma
integration itself.
Uptime Kuma's metrics endpoint only exposes a monitor's *current* status,
not its scheduled start/end time (there's no API for that), so this
deliberately doesn't try to mirror Uptime Kuma's own schedule -- it starts
a window for the duration you choose, the same bounded/required-end
window this feature has always used. Running it again while Kuma is
still in maintenance extends the same window rather than stacking a
second one; when Kuma later shows nothing in maintenance, already-active
windows are left alone rather than force-ended, since ending them isn't
something only Uptime Kuma's state should decide. Two Kuma monitors that
match the same server are deduped to one window. Monitors with no
matching server are reported back by name so nothing is silently missed,
and monitors that aren't in maintenance are ignored entirely.
The manual "Start maintenance" endpoint's start-or-extend logic (dedupe,
pruning old rows, the response shape) is now a shared
services/maintenance.ts function instead of living only in that route
handler, so the import path can't drift from how a manual window behaves.
Likewise the server-matching helper gained a small toMatchableServers()
so the existing Uptime Kuma monitors route and this new one build the
same match input the same way instead of each parsing server rows on
their own.
No schema change -- imported windows are ordinary maintenance windows;
their Uptime-Kuma origin is only in the reason text ("Imported from
Uptime Kuma (<integration>): <monitor>"), visible in the Active table and
the audit log like any other window.
Verified with 28 backend checks (the refactored manual start/extend
flow as a regression check; roles; unknown/wrong-type/disabled
integration; validation; nothing-in-maintenance; matching including
same-server dedup and unmatched monitors; re-running extends rather than
duplicating; windows left alone once Kuma exits maintenance; upstream
failure; audit entries) and by driving the real Maintenance page against
the real routers in a browser: import, re-import (extends), the
nothing-in-maintenance state, and the viewer view (no edit card, active
windows still visible). Real dev database mtime untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
import * as net from "node:net";
|
|
import type { UptimeKumaMonitor } from "../integrations/uptimekuma/adapter.js";
|
|
|
|
/** The columns of a server row this needs, so it can be tested without a live database. */
|
|
export interface MatchableServer {
|
|
id: number;
|
|
name: string;
|
|
hostname: string | null;
|
|
ips: string[];
|
|
}
|
|
|
|
const clean = (s: string) => s.trim().toLowerCase().replace(/\.$/, "");
|
|
const firstLabel = (s: string) => clean(s).split(".")[0];
|
|
|
|
/**
|
|
* Which of these servers a monitor's target host belongs to — an IP is matched against
|
|
* each server's reported addresses; anything else is matched as a hostname, against the
|
|
* server's own hostname or its short name (so "elsa" and "elsa.lab.example" both work).
|
|
* Returns null rather than guessing when nothing lines up.
|
|
*/
|
|
export function matchServerForTarget(target: string | null, servers: MatchableServer[]): MatchableServer | null {
|
|
if (!target) return null;
|
|
const t = target.trim();
|
|
if (!t) return null;
|
|
|
|
if (net.isIP(t) !== 0) {
|
|
const ip = t.toLowerCase();
|
|
return servers.find((s) => s.ips.some((a) => a.toLowerCase() === ip)) ?? null;
|
|
}
|
|
const name = clean(t);
|
|
const short = firstLabel(t);
|
|
return (
|
|
servers.find((s) => s.hostname && clean(s.hostname) === name) ??
|
|
servers.find((s) => clean(s.name) === short) ??
|
|
null
|
|
);
|
|
}
|
|
|
|
export interface MonitorWithServer extends UptimeKumaMonitor {
|
|
matchedServer: { id: number; name: string } | null;
|
|
}
|
|
|
|
export function attachMatchedServers(monitors: UptimeKumaMonitor[], servers: MatchableServer[]): MonitorWithServer[] {
|
|
return monitors.map((m) => {
|
|
const match = matchServerForTarget(m.target, servers);
|
|
return { ...m, matchedServer: match ? { id: match.id, name: match.name } : null };
|
|
});
|
|
}
|
|
|
|
/** A server row as selected with {id, name, hostname, ips: <the JSON ip_addresses column>} — turns it into what matchServerForTarget needs. */
|
|
export interface ServerRow {
|
|
id: number;
|
|
name: string;
|
|
hostname: string | null;
|
|
ips: string | null;
|
|
}
|
|
|
|
function parseServerIps(stored: string | null): string[] {
|
|
if (!stored) return [];
|
|
try {
|
|
const value = JSON.parse(stored);
|
|
return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function toMatchableServers(rows: ServerRow[]): MatchableServer[] {
|
|
return rows.map((r) => ({ id: r.id, name: r.name, hostname: r.hostname, ips: parseServerIps(r.ips) }));
|
|
}
|
|
|
|
export interface MonitorSummary {
|
|
total: number;
|
|
up: number;
|
|
down: number;
|
|
pending: number;
|
|
maintenance: number;
|
|
unknown: number;
|
|
}
|
|
|
|
export function summarizeMonitors(monitors: UptimeKumaMonitor[]): MonitorSummary {
|
|
const summary: MonitorSummary = { total: monitors.length, up: 0, down: 0, pending: 0, maintenance: 0, unknown: 0 };
|
|
for (const m of monitors) summary[m.status]++;
|
|
return summary;
|
|
}
|