feat: homeserver status banner from Uptime Kuma (#124)
CI / Build & Quality Checks (pull_request) Successful in 4m12s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 10s
CI / Playwright smoke (e2e) (pull_request) Successful in 11m58s
CI / Build & Quality Checks (pull_request) Successful in 4m12s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 10s
CI / Playwright smoke (e2e) (pull_request) Successful in 11m58s
When the homeserver, voice calls or sign-in break, or maintenance is under way, say so in the app — driven by the Kuma status page (isitup.lotusguild.org/status/matrix), managed from Kuma's UI. The client asks Kuma directly (not via our servers) so it still hears "the server is down" when our servers can't tell it. - config.json `statusPages`, keyed by homeserver: users of other servers never contact Kuma. - utils/kumaStatus.ts (pure, unit-tested): parse Kuma 2.x's public JSON; a group is down when any monitor fails two checks in a row (down+down or pending+down); maintenance = windows under way; announcements = incidents. One strip at a time: server down (connection lost AND Kuma confirms) > server having problems > maintenance > calls down > announcement; sign-in problems on the login screen only. - Wording about the user's own connection: "Connection lost … our status checks say the server is up, so it may be your internet connection" ONLY when Kuma checked the server after this client's connection dropped and it passed; a stale "up" (Kuma needs a minute or two to notice an outage) keeps the plain "Connection Lost!". - useServerStatus: polls only while visible; 5 min, 60 s while something is wrong or the connection is lost, at once when it drops; backoff; any failure = no banner (Kuma being unreachable never looks like Matrix being down); GET only, no cookies. - UI in the existing banner slot and style (ContainerColor/Line like the sync and clock banners); Details expands; dismiss for calls-down and announcements (an edited announcement comes back); calls-down note above Join; phone: one line + Details. Needs the CSP connect-src to allow https://isitup.lotusguild.org (matrix repo) before it can fetch in production; until then it fails quiet. Tests: 15 unit tests (live page layout, two-check rule, any-monitor rule, unknown/garbage, UTC beat times, stale-vs-fresh "up", priorities, login vs client, maintenance, announcements + dismiss/edit); e2e (fixtures for Kuma): calls-down strip + dismiss across reload, other homeservers make no requests, Kuma 500 → nothing, lost connection + Kuma down → critical strip instead of "Connection Lost", + fresh "up" → "may be your connection", + stale "up" → plain "Connection Lost". Unit 1315, Playwright 26 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d6548c56fd
commit
6ae0087213
@@ -35,6 +35,13 @@ export type ClientConfig = {
|
||||
*/
|
||||
desktopCallOrigin?: string;
|
||||
|
||||
/**
|
||||
* [Gitea #124] Uptime Kuma status page per homeserver, for the status banner:
|
||||
* { "<server name>": { url, slug, groups?: { homeserver, calls, login } } }.
|
||||
* Homeservers not listed never contact Kuma.
|
||||
*/
|
||||
statusPages?: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Absolute https URL of the public web app (e.g. https://chat.lotusguild.org).
|
||||
* The desktop app sets it so it can hand calls it can't make to the browser.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { kumaUrls, parseKumaStatus, ServerStatus, StatusPageConfig } from '../utils/kumaStatus';
|
||||
|
||||
/** Normal poll, and while a problem is showing (so recovery clears quickly). */
|
||||
export const STATUS_POLL_MS = 5 * 60 * 1000;
|
||||
export const STATUS_POLL_PROBLEM_MS = 60 * 1000;
|
||||
const STATUS_FETCH_TIMEOUT_MS = 10 * 1000;
|
||||
const STATUS_BACKOFF_MAX_MS = 15 * 60 * 1000;
|
||||
|
||||
const fetchJson = async (url: string, signal: AbortSignal): Promise<unknown> => {
|
||||
const res = await fetch(url, { cache: 'no-store', credentials: 'omit', signal });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
};
|
||||
|
||||
export const hasProblem = (s: ServerStatus | null): boolean =>
|
||||
!!s &&
|
||||
(s.homeserver === 'down' || s.calls === 'down' || s.login === 'down' || s.maintenance.length > 0);
|
||||
|
||||
/**
|
||||
* [Gitea #124] Poll the homeserver's Kuma status page (none configured → no
|
||||
* requests, null). Only while the page is visible; every 5 min, every 60 s
|
||||
* while something is wrong, and at once when `urgent` turns true (this
|
||||
* client's connection dropped). Any failure → null ("no banner"), with
|
||||
* backoff: Kuma being unreachable must never look like Matrix being down.
|
||||
*/
|
||||
export const useServerStatus = (
|
||||
page: StatusPageConfig | undefined,
|
||||
urgent = false,
|
||||
): ServerStatus | null => {
|
||||
const [status, setStatus] = useState<ServerStatus | null>(null);
|
||||
const pageKey = page ? `${page.url}/${page.slug}` : '';
|
||||
|
||||
useEffect(() => {
|
||||
if (!page) {
|
||||
setStatus(null);
|
||||
return undefined;
|
||||
}
|
||||
const urls = kumaUrls(page);
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let controller: AbortController | undefined;
|
||||
let failures = 0;
|
||||
let stopped = false;
|
||||
let last: ServerStatus | null = null;
|
||||
|
||||
const schedule = (ms: number) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(poll, ms);
|
||||
};
|
||||
|
||||
async function poll() {
|
||||
if (stopped) return;
|
||||
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return;
|
||||
controller?.abort();
|
||||
controller = new AbortController();
|
||||
const abort = controller;
|
||||
const timeout = setTimeout(() => abort.abort(), STATUS_FETCH_TIMEOUT_MS);
|
||||
try {
|
||||
const [pageJson, heartbeatJson] = await Promise.all([
|
||||
fetchJson(urls.page, abort.signal),
|
||||
fetchJson(urls.heartbeat, abort.signal),
|
||||
]);
|
||||
if (stopped) return;
|
||||
failures = 0;
|
||||
last = parseKumaStatus(pageJson, heartbeatJson, page!.groups);
|
||||
setStatus(last);
|
||||
// While this client's connection is lost, keep asking every minute: that
|
||||
// is how it learns Kuma has checked the server since the drop.
|
||||
schedule(hasProblem(last) || urgent ? STATUS_POLL_PROBLEM_MS : STATUS_POLL_MS);
|
||||
} catch {
|
||||
if (stopped) return;
|
||||
failures += 1;
|
||||
last = null;
|
||||
setStatus(null);
|
||||
schedule(Math.min(STATUS_POLL_PROBLEM_MS * 2 ** failures, STATUS_BACKOFF_MAX_MS));
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
const onVisible = () => {
|
||||
if (document.visibilityState === 'visible') poll();
|
||||
};
|
||||
document.addEventListener('visibilitychange', onVisible);
|
||||
poll();
|
||||
|
||||
return () => {
|
||||
stopped = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
controller?.abort();
|
||||
document.removeEventListener('visibilitychange', onVisible);
|
||||
};
|
||||
// pageKey identifies the page; the object itself is rebuilt from config.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pageKey, urgent]);
|
||||
|
||||
return status;
|
||||
};
|
||||
Reference in New Issue
Block a user