mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(landing): add a live system status indicator to the footer (#641)
Adds /api/status to the landing Pages worker, HEAD-probing demo.snapotter.com and docs.snapotter.com with a 2 second per-attempt deadline and one retry. snapotter.com is not probed; the worker answering the request is the proof it is up. The footer badge ships grey in the static HTML and only upgrades once the route answers. A rejected fetch, a non-ok response, an unparseable body, and an unrecognized verdict all leave it grey, so it never claims green on its own. Color lives in the dot, never the label: `--color-success` scores 4.498:1 against the footer's `--color-background-alt`, just under AA. Four labels across 21 locales.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
// biome-ignore-all lint/correctness/noUnusedImports: Astro template consumes component imports.
|
||||
// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values.
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher.astro";
|
||||
import StatusIndicator from "@/components/StatusIndicator.astro";
|
||||
import { t } from "@/i18n";
|
||||
import { enOnlyHref, localizeHref } from "@/lib/i18n-page";
|
||||
|
||||
@@ -156,6 +157,8 @@ const columns = [
|
||||
{t(locale, "footer.copyright", { year })}
|
||||
</p>
|
||||
<div class="flex items-center gap-4">
|
||||
<StatusIndicator locale={locale} />
|
||||
<span class="hidden h-3 w-px bg-border sm:block" aria-hidden="true"></span>
|
||||
<p class="text-xs text-muted">
|
||||
contact@snapotter.com
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values.
|
||||
import { t } from "@/i18n";
|
||||
|
||||
// Required rather than optional-with-an-"en"-default: an omitted prop would
|
||||
// typecheck, lint, render, and quietly serve English on all 20 localized page
|
||||
// trees. LanguageSwitcher, three lines away in the same footer cluster,
|
||||
// declares locale the same way.
|
||||
interface Props {
|
||||
locale: string;
|
||||
}
|
||||
const { locale } = Astro.props;
|
||||
|
||||
// Every label is resolved server-side and handed to the script, so the client
|
||||
// never needs a second request or a translation table of its own.
|
||||
const labels = {
|
||||
checking: t(locale, "footer.status.checking"),
|
||||
operational: t(locale, "footer.status.operational"),
|
||||
partial: t(locale, "footer.status.partial"),
|
||||
down: t(locale, "footer.status.down"),
|
||||
};
|
||||
---
|
||||
|
||||
<span
|
||||
class="flex items-center gap-2 text-xs text-muted"
|
||||
data-status-indicator
|
||||
data-status="checking"
|
||||
title="snapotter.com, demo.snapotter.com, docs.snapotter.com"
|
||||
>
|
||||
<span class="status-dot" data-status-dot aria-hidden="true"></span>
|
||||
<span data-status-label>{labels.checking}</span>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
/* The dot carries every state color and the label never does. --color-success
|
||||
is 4.498:1 on --color-background-alt, the footer's background, just under
|
||||
AA's 4.5. palette-contrast.test.ts does not cover that pair: it checks
|
||||
success against --color-background, where it passes at 4.967. So the guard
|
||||
here is the toHaveCSS assertion in tests/e2e-landing/status-indicator.spec.ts,
|
||||
not the unit test. Adding the pair to the unit test would fail by design,
|
||||
since success-as-text-on-alt is not a supported combination.
|
||||
|
||||
Every state clears WCAG 1.4.11's 3:1 floor for non-text against
|
||||
--color-background-alt: muted 4.97, success 4.50, primary-ink 4.57,
|
||||
danger 4.73. */
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
flex: none;
|
||||
border-radius: 9999px;
|
||||
background: var(--color-muted);
|
||||
}
|
||||
[data-status="operational"] .status-dot {
|
||||
background: var(--color-success);
|
||||
}
|
||||
[data-status="partial"] .status-dot {
|
||||
background: var(--color-primary-ink);
|
||||
}
|
||||
[data-status="down"] .status-dot {
|
||||
background: var(--color-danger);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script is:inline define:vars={{ labels }}>
|
||||
(() => {
|
||||
const root = document.querySelector("[data-status-indicator]");
|
||||
if (!root) return;
|
||||
fetch("/api/status")
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
// Route unreachable or body unparseable: stay grey rather than guess.
|
||||
// Scoped to the fetch alone. A trailing catch would also swallow a DOM
|
||||
// bug in the handler below and pin the badge on "Checking status" with
|
||||
// nothing in the console.
|
||||
.catch(() => null)
|
||||
.then((data) => {
|
||||
const status = data?.status ?? "";
|
||||
// Own properties only. A payload of {"status":"toString"} would
|
||||
// otherwise resolve up the prototype chain and render the function.
|
||||
const label = Object.hasOwn(labels, status) ? labels[status] : null;
|
||||
// An unknown or absent verdict leaves the grey default alone. The badge
|
||||
// never claims a state nothing told it.
|
||||
if (!label) return;
|
||||
root.dataset.status = status;
|
||||
root.querySelector("[data-status-label]").textContent = label;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user