mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-09-01 17:08:17 +00:00
chore(website): spring-clean public pages
This commit is contained in:
@@ -171,3 +171,91 @@ start from the live state.
|
||||
visible`). The report/evidence commit follows this append; no GitHub push.
|
||||
- **Evidence:** `SPRING-MAPFEED-2026-08-06.png` shows the live map with a real
|
||||
message-first feed card and compact secondary metadata.
|
||||
|
||||
## Wave 3 investigation and implementation design — items 10/12/14 (2026-08-06)
|
||||
|
||||
### Item 10 — observation register: resolved by absence
|
||||
|
||||
The frontend route table contains the public pages `/`, `/install`, `/stats`,
|
||||
`/feed`, `/repeater`, `/companion`, `/docs`, `/open-source`, `/spam`,
|
||||
`/topology`, and `/login`, plus redirects for `/regions`, `/about`, `/mqtt`,
|
||||
and `/status`; it contains no `/register` or `/observations` page. The navigation
|
||||
has no observation/register entry. A case-insensitive search of `frontend/src`
|
||||
found only data/UI uses of “observations” in topology, packet detail, feed path
|
||||
models, and map statistics, plus the observer-station registration form embedded
|
||||
in `UKCompanionPage`; none is a dedicated observation register page. Stats,
|
||||
repeater search, and owner portal are ordinary existing pages with no hidden
|
||||
observation-register route. Per the item definition, this is resolved by absence;
|
||||
no page was invented or changed.
|
||||
|
||||
### Item 12 — Spam Watch: keep
|
||||
|
||||
Live host baseline at `http://127.0.0.1:3000/api/spam/messages/status` returned
|
||||
HTTP 200 JSON:
|
||||
`{"ongoing":false,"activeIncidents":0,"totalIncidents":0,"messagesLast24h":0,"observersInvolved":0,"lastIncidentAt":null,"updatedAt":"2026-08-06T12:49:27.141Z"}`.
|
||||
The companion request used by the page,
|
||||
`/api/spam/messages/incidents?limit=200`, also returned HTTP 200 JSON with
|
||||
`filters` (`status: all`, `minConfidence: 0.5`, `limit: 200`, `offset: 0`),
|
||||
`returned: 0`, and an empty `incidents` array. A Googlebot Playwright run against
|
||||
`https://ukmesh.com/spam` observed HTTP 200 JSON for both page API requests,
|
||||
rendered the `Spam Watch` heading and `No ongoing spam detected`, and rendered
|
||||
no alert. This is a useful, live transparency page even when the current result
|
||||
is an empty/clean state, so it will remain unchanged unless final verification
|
||||
finds a regression.
|
||||
|
||||
### Item 14 — Health page removal: designed frontend-only
|
||||
|
||||
Remove `frontend/src/pages/StatusPage.tsx`; remove its lazy import and element,
|
||||
the `health` route component type, `/health` content route, and the obsolete
|
||||
`/status -> /health` redirect. Remove `showHealth` from `SiteLayout` and its
|
||||
`/health` navigation item, and remove the now-unused `showHealth` prop from
|
||||
`UKLayout`. The separate `healthcheck.ukmesh.com` external link and the
|
||||
`~/ukmesh/meshcore-health-check` application are explicitly out of scope and
|
||||
will not be touched. Acceptance evidence is a zero-result `rg -F '/health'
|
||||
frontend` search plus a public browser assertion that `/health` renders the
|
||||
normal not-found state and no Health nav entry, while `/spam` retains its clean
|
||||
live state.
|
||||
|
||||
No backend change is planned. After implementation, run the required frontend
|
||||
quality gates, build both real-tagged images, compare their shipped bundles,
|
||||
deploy only the services whose bundle changed, and then perform the required
|
||||
health/HopReach/API/browser checks before recording the screenshot, digests, and
|
||||
local commit below.
|
||||
|
||||
## Wave 3 completion — items 10/12/14 (2026-08-06)
|
||||
|
||||
- **[DONE] Item 10 — observation register:** resolved by absence. The route
|
||||
manifest and nav have no `/register` or `/observations` page; the frontend
|
||||
search found only ordinary observation data fields and the embedded observer
|
||||
station registration form. No page was invented.
|
||||
- **[DONE] Item 12 — Spam Watch:** kept. Host curl returned HTTP 200 with
|
||||
`ongoing: false`, zero active/total incidents, zero messages in the last 24h,
|
||||
zero observers involved, and a current `updatedAt`; the incidents endpoint
|
||||
returned HTTP 200 with a valid empty list. Googlebot Playwright against the
|
||||
live public URL received HTTP 200 JSON for both requests and rendered the
|
||||
heading plus `No ongoing spam detected` with no alert.
|
||||
- **[DONE] Item 14 — Health page removal:** deleted `StatusPage.tsx`, its lazy
|
||||
route wiring, `/health` manifest entry, `/status -> /health` redirect, nav
|
||||
item/flag, unused status CSS, and stale E2E page checks. Direct `/health`
|
||||
page-link searches in `frontend` are empty. The external Health Check link
|
||||
and separate health-check app remain out of scope.
|
||||
- **Quality gates:** frontend `npx tsc --noEmit`, `npm test` (77/77),
|
||||
`npm run lint:css`, and `npm run build` all pass. No backend files changed.
|
||||
- **Deployment:** built both `spring-pages` tags. The new not-found marker was
|
||||
present in both asset sets, so `app-ukmesh` and `website-ukmesh` were both
|
||||
deployed; no backend service was changed. Final app digest:
|
||||
`sha256:5f278c9796c67402c644961555feb8a52c4ee6eb4cc1f41c94b9b79276c80b55`;
|
||||
final website digest:
|
||||
`sha256:56885f493cca824e2e7cb7ce6cda7b08748747762b00ffa84bdfb9432db9e67b`.
|
||||
`.env` changed only `APP_IMAGE` and `WEBSITE_IMAGE`; `BACKEND_IMAGE` and
|
||||
secret lines were untouched.
|
||||
- **Live verification:** `/api/health` was healthy, HopReach returned 200,
|
||||
`/health` and `/status` rendered the normal not-found state with no exact
|
||||
Health nav link, and `/spam` rendered its live clean state with both API calls
|
||||
at HTTP 200 and no alert. The SPA fallback returns document HTTP 200 for
|
||||
unknown routes; the rendered not-found state proves the page route is gone.
|
||||
- **Evidence:** `SPRING-PAGES-2026-08-06.png` contains the real Googlebot
|
||||
captures for the removed `/health` state and retained `/spam` state. Summary:
|
||||
`SPRING-PAGES-2026-08-06.md`.
|
||||
- **Local commit:** to be recorded after the final scoped commit; no GitHub
|
||||
push.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# UKMesh Spring Pages — 2026-08-06
|
||||
|
||||
Scope: items 10, 12, and 14 on `/home/ben/ukmesh/meshcore-analytics`, branch
|
||||
`main`. The separate `~/ukmesh/meshcore-health-check` application was not
|
||||
modified. No GitHub push was performed.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Item 10 — observation register: resolved by absence
|
||||
|
||||
There is no dedicated observation-register page. The `PUBLIC_ROUTES` manifest
|
||||
contains no `/register` or `/observations` route, and the site navigation has no
|
||||
observation/register entry. A case-insensitive `rg` search across `frontend/src`
|
||||
found only ordinary observation data labels and models in topology, stats,
|
||||
packet detail, feed paths, and map statistics, plus the observer-station
|
||||
registration form embedded in `UKCompanionPage`. Stats, repeater search, and
|
||||
owner portal have no hidden register page. No page was invented or changed.
|
||||
|
||||
### Item 12 — Spam Watch: kept
|
||||
|
||||
The live host endpoint returned HTTP 200 and a valid page-shaped payload:
|
||||
|
||||
```json
|
||||
{"ongoing":false,"activeIncidents":0,"totalIncidents":0,"messagesLast24h":0,"observersInvolved":0,"lastIncidentAt":null,"updatedAt":"2026-08-06T12:49:27.141Z"}
|
||||
```
|
||||
|
||||
The page’s incidents request, `/api/spam/messages/incidents?limit=200`, also
|
||||
returned HTTP 200 with `minConfidence: 0.5`, `limit: 200`, `returned: 0`, and an
|
||||
empty `incidents` array. A Googlebot Playwright check of the real public URL
|
||||
observed HTTP 200 JSON for both requests, rendered `Spam Watch` and
|
||||
`No ongoing spam detected`, and rendered no alert/error element. The page is a
|
||||
useful transparency surface in this valid clean state, so no Spam code change
|
||||
was necessary.
|
||||
|
||||
### Item 14 — Health page: removed
|
||||
|
||||
Removed the page module, lazy import, route component/type, `/health` content
|
||||
route, obsolete `/status -> /health` redirect, `showHealth` prop, Health nav
|
||||
entry, unused UK layout flag, page-specific status CSS, and stale E2E page
|
||||
fixtures/assertions. Direct page-link searches for `/health` in `frontend`
|
||||
return no results. The external `https://healthcheck.ukmesh.com` Health Check
|
||||
link remains intentionally because it belongs to the separate application.
|
||||
|
||||
## Quality and deployment
|
||||
|
||||
- `cd frontend && npx tsc --noEmit` — passed.
|
||||
- `cd frontend && npm test` — 77/77 passed.
|
||||
- `cd frontend && npm run lint:css` — passed.
|
||||
- `cd frontend && npm run build` — passed; only existing large-chunk warnings.
|
||||
- Built both requested tags: `meshcore-analytics-app:spring-pages` and
|
||||
`meshcore-analytics-website:spring-pages`.
|
||||
- The new not-found marker was present in both shipped asset sets, so both
|
||||
`app-ukmesh` and `website-ukmesh` bundles changed and both were deployed.
|
||||
No backend image/service was changed.
|
||||
- Final deployed image IDs:
|
||||
- app: `sha256:5f278c9796c67402c644961555feb8a52c4ee6eb4cc1f41c94b9b79276c80b55`
|
||||
- website: `sha256:56885f493cca824e2e7cb7ce6cda7b08748747762b00ffa84bdfb9432db9e67b`
|
||||
- `.env` was updated only for `APP_IMAGE` and `WEBSITE_IMAGE`; `BACKEND_IMAGE`
|
||||
and secret/configuration lines were left unchanged.
|
||||
|
||||
## Live verification
|
||||
|
||||
- `GET http://127.0.0.1:3000/api/health` — HTTP 200, `status: healthy`.
|
||||
- `GET http://127.0.0.1:3000/hopreach/api/nodes` — HTTP 200.
|
||||
- Googlebot Playwright against `https://ukmesh.com/health` — the SPA transport
|
||||
returned 200, but the rendered page was the normal `That page is not on the
|
||||
mesh` not-found state and the exact `Health` nav link count was zero.
|
||||
- Googlebot Playwright against `https://ukmesh.com/status` — same normal
|
||||
not-found state; no legacy redirect to the removed Health page.
|
||||
- Googlebot Playwright against `https://ukmesh.com/spam` — document HTTP 200;
|
||||
status and incidents API responses HTTP 200 with JSON content type; heading
|
||||
and clean state visible; alert count zero.
|
||||
|
||||
The browser emitted the same pre-existing unauthenticated owner-session and
|
||||
service-worker MIME console noise seen in the pre-deploy baseline; neither
|
||||
affected page data loading or rendered state. Evidence image:
|
||||
[SPRING-PAGES-2026-08-06.png](./SPRING-PAGES-2026-08-06.png).
|
||||
|
||||
## Local commit
|
||||
|
||||
Implementation and evidence were committed locally after verification. No
|
||||
remote push was attempted.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
@@ -5,7 +5,6 @@ export type PublicRouteComponent =
|
||||
| 'companion'
|
||||
| 'install'
|
||||
| 'docs'
|
||||
| 'health'
|
||||
| 'owner'
|
||||
| 'open-source'
|
||||
| 'stats'
|
||||
@@ -92,13 +91,6 @@ export const PUBLIC_ROUTES: readonly PublicRouteDefinition[] = [
|
||||
description: 'Explore recent viable repeater relationships, highly connected relay hubs, and observed MeshCore network topology across the UK.',
|
||||
sitemap: true,
|
||||
},
|
||||
{
|
||||
path: '/health',
|
||||
component: 'health',
|
||||
title: 'Platform Status — UK Mesh Network',
|
||||
description: 'Public platform health, packet-ingest freshness, synthetic journeys, and background-worker status for UKMesh analytics.',
|
||||
sitemap: true,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: 'owner',
|
||||
@@ -109,7 +101,6 @@ export const PUBLIC_ROUTES: readonly PublicRouteDefinition[] = [
|
||||
{ path: '/regions', redirectTo: '/', sitemap: false },
|
||||
{ path: '/about', redirectTo: '/', sitemap: false },
|
||||
{ path: '/mqtt', redirectTo: '/install', sitemap: false },
|
||||
{ path: '/status', redirectTo: '/health', sitemap: false },
|
||||
] as const;
|
||||
|
||||
export const PUBLIC_CONTENT_ROUTES = PUBLIC_ROUTES.filter(
|
||||
|
||||
@@ -29,7 +29,6 @@ const UKCompanionPage = lazy(() => import('./pages/ukmesh/UKCompanionPage.js').t
|
||||
const UKBestPracticePage = lazy(() => import('./pages/ukmesh/UKBestPracticePage.js').then(({ UKBestPracticePage: Component }) => ({ default: Component })));
|
||||
const SpamPage = lazy(() => import('./pages/SpamTransparencyPage.js').then(({ SpamPage: Component }) => ({ default: Component })));
|
||||
const TopologyPage = lazy(() => import('./pages/TopologyPage.js').then(({ TopologyPage: Component }) => ({ default: Component })));
|
||||
const StatusPage = lazy(() => import('./pages/StatusPage.js').then(({ StatusPage: Component }) => ({ default: Component })));
|
||||
const NotFoundPage = lazy(() => import('./pages/NotFoundPage.js').then(({ NotFoundPage: Component }) => ({ default: Component })));
|
||||
|
||||
const PUBLIC_ROUTE_ELEMENTS: Record<PublicRouteComponent, React.ReactElement> = {
|
||||
@@ -39,7 +38,6 @@ const PUBLIC_ROUTE_ELEMENTS: Record<PublicRouteComponent, React.ReactElement> =
|
||||
companion: <UKCompanionPage />,
|
||||
install: <UKInstallPage />,
|
||||
docs: <UKBestPracticePage />,
|
||||
health: <StatusPage />,
|
||||
owner: <OwnerPortalPage />,
|
||||
'open-source': <OpenSourcePage />,
|
||||
stats: <StatsPage />,
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { LoadingIndicator } from '../components/LoadingIndicator.js';
|
||||
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
import { useRuntimeFeatures } from '../config/runtimeFeatures.js';
|
||||
import { getCurrentSite } from '../config/site.js';
|
||||
import { useVisibilityPoll } from '../hooks/useVisibilityPoll.js';
|
||||
import { fetchJson, withScopeParams } from '../utils/api.js';
|
||||
import './network-intelligence.css';
|
||||
|
||||
type PublicHealthPayload = {
|
||||
status: 'healthy' | 'degraded' | 'critical';
|
||||
generatedAt: string;
|
||||
maintenance: { active: boolean; message: string | null };
|
||||
incidents: Array<{ code: string; severity: 'warning' | 'critical' }>;
|
||||
components: {
|
||||
ingest: { status: string };
|
||||
workers: { status: string };
|
||||
storage: { status: string };
|
||||
};
|
||||
};
|
||||
type FirmwarePayload = { total: number; versions: Array<{ hardware_model: string; firmware_version: string; count: number }> };
|
||||
type FirmwareDistributionRow = {
|
||||
firmware_version: string;
|
||||
count: number;
|
||||
hardware_models: string[];
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function hasStringStatus(value: unknown): value is { status: string } {
|
||||
return isRecord(value) && typeof value['status'] === 'string';
|
||||
}
|
||||
|
||||
function isPublicHealthPayload(value: unknown): value is PublicHealthPayload {
|
||||
if (!isRecord(value) || !isRecord(value['maintenance']) || !isRecord(value['components'])) return false;
|
||||
const components = value['components'];
|
||||
return isRecord(value)
|
||||
&& (value['status'] === 'healthy' || value['status'] === 'degraded' || value['status'] === 'critical')
|
||||
&& typeof value['generatedAt'] === 'string'
|
||||
&& typeof value['maintenance']['active'] === 'boolean'
|
||||
&& (typeof value['maintenance']['message'] === 'string' || value['maintenance']['message'] === null)
|
||||
&& Array.isArray(value['incidents'])
|
||||
&& value['incidents'].every((incident) => (
|
||||
isRecord(incident)
|
||||
&& typeof incident['code'] === 'string'
|
||||
&& (incident['severity'] === 'warning' || incident['severity'] === 'critical')
|
||||
))
|
||||
&& hasStringStatus(components['ingest'])
|
||||
&& hasStringStatus(components['workers'])
|
||||
&& hasStringStatus(components['storage']);
|
||||
}
|
||||
|
||||
function isFirmwarePayload(value: unknown): value is FirmwarePayload {
|
||||
return isRecord(value)
|
||||
&& typeof value['total'] === 'number'
|
||||
&& Array.isArray(value['versions']);
|
||||
}
|
||||
|
||||
function aggregateFirmwareVersions(versions: FirmwarePayload['versions']): FirmwareDistributionRow[] {
|
||||
const byVersion = new Map<string, FirmwareDistributionRow>();
|
||||
for (const row of versions) {
|
||||
const firmwareVersion = row.firmware_version || 'Unknown';
|
||||
const existing = byVersion.get(firmwareVersion);
|
||||
if (existing) {
|
||||
existing.count += row.count;
|
||||
if (!existing.hardware_models.includes(row.hardware_model)) {
|
||||
existing.hardware_models.push(row.hardware_model);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
byVersion.set(firmwareVersion, {
|
||||
firmware_version: firmwareVersion,
|
||||
count: row.count,
|
||||
hardware_models: [row.hardware_model],
|
||||
});
|
||||
}
|
||||
return Array.from(byVersion.values());
|
||||
}
|
||||
|
||||
function isUnknownFirmwareVersion(version: string): boolean {
|
||||
return version.toLowerCase() === 'unknown';
|
||||
}
|
||||
|
||||
function statusPresentation(status: string): { dot: string; label: string } {
|
||||
switch (status) {
|
||||
case 'ok': return { dot: 'ok', label: 'Operational' };
|
||||
case 'running': return { dot: 'ok', label: 'Running' };
|
||||
case 'idle': return { dot: 'ok', label: 'Ready' };
|
||||
case 'warning': return { dot: 'warning', label: 'Needs attention' };
|
||||
case 'stale': return { dot: 'warning', label: 'Delayed' };
|
||||
case 'critical': return { dot: 'critical', label: 'Critical' };
|
||||
case 'failed': return { dot: 'critical', label: 'Failed' };
|
||||
default: return { dot: 'unknown', label: 'Checking' };
|
||||
}
|
||||
}
|
||||
|
||||
function reportTime(generatedAt: string): string {
|
||||
const value = new Date(generatedAt);
|
||||
return Number.isNaN(value.getTime())
|
||||
? 'just now'
|
||||
: value.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
export const StatusPage: React.FC = () => {
|
||||
const site = getCurrentSite();
|
||||
const network = site.networkFilter ?? site.network;
|
||||
const observer = site.observerId;
|
||||
const { privacyGeneration } = useRuntimeFeatures();
|
||||
const [data, setData] = useState<PublicHealthPayload | null>(null);
|
||||
const [error, setError] = useState(false);
|
||||
const [firmware, setFirmware] = useState<FirmwarePayload | null>(null);
|
||||
const [includeUnknownFirmware, setIncludeUnknownFirmware] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setData(null);
|
||||
setFirmware(null);
|
||||
setError(false);
|
||||
setIncludeUnknownFirmware(false);
|
||||
}, [network, observer, privacyGeneration]);
|
||||
|
||||
useVisibilityPoll(async (signal) => {
|
||||
const next = await fetchJson<PublicHealthPayload>(
|
||||
withScopeParams('/api/health', { network, observer }),
|
||||
{ cache: 'no-store', signal },
|
||||
{ timeoutMs: 15_000, maxBytes: 2 * 1024 * 1024, validate: isPublicHealthPayload },
|
||||
);
|
||||
if (signal.aborted) return;
|
||||
setData(next);
|
||||
setError(false);
|
||||
}, {
|
||||
scopeKey: `platform-health:${network}:${observer ?? 'all'}:${privacyGeneration}`,
|
||||
intervalMs: 60_000,
|
||||
timeoutMs: 15_000,
|
||||
onError: () => setError(true),
|
||||
});
|
||||
|
||||
useVisibilityPoll(async (signal) => {
|
||||
const next = await fetchJson<FirmwarePayload>(
|
||||
withScopeParams('/api/repeaters/firmware', { network, observer }),
|
||||
{ cache: 'no-store', signal },
|
||||
{ timeoutMs: 15_000, maxBytes: 2 * 1024 * 1024, validate: isFirmwarePayload },
|
||||
);
|
||||
if (!signal.aborted) setFirmware(next);
|
||||
}, {
|
||||
scopeKey: `firmware-health:${network}:${observer ?? 'all'}:${privacyGeneration}`,
|
||||
intervalMs: 10 * 60_000,
|
||||
timeoutMs: 15_000,
|
||||
});
|
||||
|
||||
const firmwareDistribution = firmware ? aggregateFirmwareVersions(firmware.versions) : [];
|
||||
const unknownFirmware = firmwareDistribution.find((row) => isUnknownFirmwareVersion(row.firmware_version));
|
||||
const visibleFirmwareDistribution = includeUnknownFirmware
|
||||
? firmwareDistribution
|
||||
: firmwareDistribution.filter((row) => !isUnknownFirmwareVersion(row.firmware_version));
|
||||
const xAxisInterval = Math.max(0, Math.ceil(visibleFirmwareDistribution.length / 8) - 1);
|
||||
|
||||
return (
|
||||
<div className="status-page site-content">
|
||||
<header><p className="topology-page__eyebrow">Public operations</p><h1>Platform status</h1><p>Service health, ingest freshness, and worker state without exposing private infrastructure details.</p></header>
|
||||
{!data && !error && <LoadingIndicator label="Checking platform status…" variant="block" />}
|
||||
{error && !data && <div className="status-page__banner status-page__banner--critical" role="alert">Status data is currently unavailable.</div>}
|
||||
{data && (
|
||||
<>
|
||||
<div className={`status-page__banner status-page__banner--${data.status}`} role="status">
|
||||
<strong>{data.status === 'healthy' ? 'All monitored systems operational' : `Platform ${data.status}`}</strong>
|
||||
<span>Updated {reportTime(data.generatedAt)}</span>
|
||||
</div>
|
||||
{data.maintenance.active && <div className="status-page__maintenance"><strong>Planned maintenance</strong><span>{data.maintenance.message ?? 'Maintenance is currently in progress.'}</span></div>}
|
||||
{data.incidents.length > 0 && <section className="status-page__problems"><h2>Current notices</h2>{data.incidents.map((incident) => <article key={`${incident.code}:${incident.severity}`} className={`status-page__problem status-page__problem--${incident.severity}`}><strong>{incident.code.replace(/_/g, ' ')}</strong><span>{incident.severity === 'critical' ? 'A monitored service is disrupted.' : 'A monitored service needs attention.'}</span></article>)}</section>}
|
||||
<div className="status-page__grid">
|
||||
{([
|
||||
['Public ingest', 'Packet intake and freshness', data.components.ingest.status],
|
||||
['Background workers', 'Scheduled processing services', data.components.workers.status],
|
||||
['Storage', 'Durable platform storage', data.components.storage.status],
|
||||
] as const).map(([title, description, status]) => {
|
||||
const presentation = statusPresentation(status);
|
||||
return (
|
||||
<section key={title}>
|
||||
<h2>{title}</h2>
|
||||
<div className="status-page__checks">
|
||||
<div>
|
||||
<span className={`status-page__dot status-page__dot--${presentation.dot}`} />
|
||||
<strong>{presentation.label}</strong>
|
||||
<small>{description}</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<section className="status-page__firmware">
|
||||
<h2>Repeater firmware distribution</h2>
|
||||
{firmware && firmware.versions.length > 0 ? (
|
||||
<>
|
||||
{unknownFirmware && (
|
||||
<p style={{ margin: '0 0 12px', color: 'var(--text-secondary)', fontSize: '12px' }}>
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeUnknownFirmware}
|
||||
onChange={(event) => setIncludeUnknownFirmware(event.target.checked)}
|
||||
style={{ accentColor: 'var(--accent)' }}
|
||||
/>
|
||||
<span>Include Unknown ({unknownFirmware.count.toLocaleString()})</span>
|
||||
</label>
|
||||
</p>
|
||||
)}
|
||||
{visibleFirmwareDistribution.length > 0 ? (
|
||||
<>
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<BarChart data={visibleFirmwareDistribution} margin={{ left: 8, right: 8, bottom: 88 }}>
|
||||
<CartesianGrid stroke="rgba(255,255,255,.1)" />
|
||||
<XAxis
|
||||
dataKey="firmware_version"
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
interval={xAxisInterval}
|
||||
height={96}
|
||||
minTickGap={10}
|
||||
tick={{ fill: 'var(--text-secondary)', fontSize: 11 }}
|
||||
/>
|
||||
<YAxis allowDecimals={false} />
|
||||
<Tooltip formatter={(value: number) => [value.toLocaleString(), 'Repeaters']} />
|
||||
<Bar dataKey="count" name="Repeaters" fill="var(--color-primary)" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="ui-visually-hidden">
|
||||
<table>
|
||||
<caption>Repeater firmware distribution{includeUnknownFirmware ? '' : ' (Unknown excluded)'}</caption>
|
||||
<thead><tr><th scope="col">Firmware</th><th scope="col">Hardware models</th><th scope="col">Repeaters</th></tr></thead>
|
||||
<tbody>
|
||||
{visibleFirmwareDistribution.map((row) => (
|
||||
<tr key={row.firmware_version}>
|
||||
<td>{row.firmware_version}</td>
|
||||
<td>{row.hardware_models.join(', ')}</td>
|
||||
<td>{row.count}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p role="status">Only repeaters with unknown firmware were reported. Select "Include Unknown" to show that bucket.</p>
|
||||
)}
|
||||
</>
|
||||
) : <p>Firmware telemetry is not yet available.</p>}
|
||||
</section>
|
||||
<p className="status-page__privacy">Status values are deliberately aggregated. Hostnames, addresses, credentials, and private node identities are never included.</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -69,45 +69,6 @@
|
||||
.topology-page__validation-table-head { color: var(--text-muted) !important; font: 11px var(--font-mono); letter-spacing: 0.06em; text-transform: uppercase; }
|
||||
.topology-page__validation-table strong { color: var(--amber); font: 11px var(--font-mono); text-transform: uppercase; }
|
||||
|
||||
.status-page { padding-top: 48px; padding-bottom: 64px; }
|
||||
.status-page > header { margin-bottom: 22px; }
|
||||
.status-page > header h1 { margin: 4px 0 8px; }
|
||||
.status-page > header > p:last-child { color: var(--text-secondary); }
|
||||
.status-page__banner { display: flex; justify-content: space-between; gap: 15px; padding: 14px 18px; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg-panel); }
|
||||
.status-page__banner strong { color: var(--text-primary); }
|
||||
.status-page__banner span { color: var(--text-muted); font: 10px var(--font-mono); }
|
||||
.status-page__banner--healthy { border-color: rgba(0, 230, 118, 0.45); }
|
||||
.status-page__banner--degraded { border-color: rgba(255, 179, 0, 0.55); }
|
||||
.status-page__banner--critical { border-color: rgba(255, 23, 68, 0.6); color: #fca5a5; }
|
||||
.status-page__maintenance { display: flex; gap: 16px; margin-top: 10px; padding: 11px 16px; border-radius: var(--radius); background: var(--amber-dim); color: var(--amber); }
|
||||
.status-page__maintenance span { color: var(--text-secondary); }
|
||||
.status-page__problems { margin-top: 20px; }
|
||||
.status-page__problems h2 { margin-bottom: 8px; font-size: 15px; }
|
||||
.status-page__problem { display: flex; gap: 14px; padding: 9px 12px; border-left: 3px solid var(--amber); background: var(--bg-panel); }
|
||||
.status-page__problem + .status-page__problem { margin-top: 4px; }
|
||||
.status-page__problem strong { min-width: 180px; color: var(--amber); font: 10px var(--font-mono); text-transform: uppercase; }
|
||||
.status-page__problem span { color: var(--text-secondary); font-size: 11px; }
|
||||
.status-page__problem--critical { border-left-color: var(--danger); }
|
||||
.status-page__problem--critical strong { color: var(--danger); }
|
||||
.status-page__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 20px; }
|
||||
.status-page__grid > section { padding: 18px; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg-panel); }
|
||||
.status-page__grid h2 { margin: 0 0 12px; font-size: 14px; }
|
||||
.status-page__grid dl div { display: flex; justify-content: space-between; gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--border); }
|
||||
.status-page__grid dt { color: var(--text-muted); }
|
||||
.status-page__grid dd { color: var(--text-primary); font-family: var(--font-mono); }
|
||||
.status-page__checks > div { display: grid; grid-template-columns: 10px minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 7px 0; border-bottom: 1px solid var(--border); }
|
||||
.status-page__checks strong { overflow: hidden; color: var(--text-secondary); font: 11px var(--font-mono); text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; }
|
||||
.status-page__checks small { color: var(--text-muted); }
|
||||
.status-page__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); }
|
||||
.status-page__dot--ok { background: var(--online); box-shadow: 0 0 8px rgba(0, 230, 118, 0.45); }
|
||||
.status-page__dot--warning,
|
||||
.status-page__dot--stale,
|
||||
.status-page__dot--unknown { background: var(--amber); }
|
||||
.status-page__dot--critical,
|
||||
.status-page__dot--failed,
|
||||
.status-page__dot--disabled { background: var(--danger); }
|
||||
.status-page__privacy { margin-top: 16px; color: var(--text-muted); font-size: 11px; }
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.topology-page__workspace { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -122,16 +83,4 @@
|
||||
.topology-page__validation-head { flex-direction: column; gap: 8px; }
|
||||
.topology-page__validation-stats { grid-template-columns: 1fr 1fr; }
|
||||
.topology-page__validation-table [role='row'] { grid-template-columns: 1fr 1fr; }
|
||||
.status-page { padding-top: 28px; }
|
||||
.status-page__banner,
|
||||
.status-page__maintenance,
|
||||
.status-page__problem { flex-direction: column; gap: 5px; }
|
||||
.status-page__grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.status-page__firmware {
|
||||
margin-top: 18px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ type SiteLayoutProps = {
|
||||
showAbout?: boolean;
|
||||
showInstall?: boolean;
|
||||
showMqtt?: boolean;
|
||||
showHealth?: boolean;
|
||||
showOpenSource?: boolean;
|
||||
showBestPractice?: boolean;
|
||||
showPackets: boolean;
|
||||
@@ -48,7 +47,6 @@ export const SiteLayout: React.FC<SiteLayoutProps> = ({
|
||||
showAbout = true,
|
||||
showInstall = true,
|
||||
showMqtt = true,
|
||||
showHealth = true,
|
||||
showOpenSource = true,
|
||||
showBestPractice = false,
|
||||
showPackets,
|
||||
@@ -81,7 +79,6 @@ export const SiteLayout: React.FC<SiteLayoutProps> = ({
|
||||
{ to: '/about', label: 'What is MeshCore', enabled: showAbout },
|
||||
{ to: '/install', label: 'Install', enabled: showInstall },
|
||||
{ to: '/mqtt', label: 'MQTT', enabled: showMqtt },
|
||||
{ to: '/health', label: 'Health', enabled: showHealth },
|
||||
{ to: '/packets', label: 'Packets', enabled: showPackets },
|
||||
{ to: '/open-source', label: 'Open Source', enabled: showOpenSource },
|
||||
{ to: '/stats', label: 'Stats', enabled: showStats },
|
||||
|
||||
@@ -27,7 +27,6 @@ export const UKLayout: React.FC = () => {
|
||||
showInstall
|
||||
showBestPractice
|
||||
showMqtt={false}
|
||||
showHealth
|
||||
showPackets={false}
|
||||
showOpenSource={false}
|
||||
showStats={false}
|
||||
|
||||
@@ -91,21 +91,6 @@ async function fulfillApi(route: Route) {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (pathname === '/api/health') {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
status: 'healthy',
|
||||
generatedAt: '2026-07-18T12:00:00Z',
|
||||
maintenance: { active: false, message: null },
|
||||
incidents: [],
|
||||
components: {
|
||||
ingest: { status: 'ok' },
|
||||
workers: { status: 'running' },
|
||||
storage: { status: 'ok' },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (pathname === '/api/topology') {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
@@ -229,7 +214,6 @@ const publicRoutes = [
|
||||
'/companion',
|
||||
'/install',
|
||||
'/docs',
|
||||
'/health',
|
||||
'/login',
|
||||
'/open-source',
|
||||
'/stats',
|
||||
|
||||
@@ -62,18 +62,6 @@ const chartStats = {
|
||||
},
|
||||
};
|
||||
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
generatedAt: '2026-07-18T12:00:00Z',
|
||||
maintenance: { active: false, message: null },
|
||||
incidents: [],
|
||||
components: {
|
||||
ingest: { status: 'ok' },
|
||||
workers: { status: 'running' },
|
||||
storage: { status: 'ok' },
|
||||
},
|
||||
};
|
||||
|
||||
const repeaterNodes = Array.from({ length: 12 }, (_, index) => ({
|
||||
node_id: `node-${index}`,
|
||||
name: `Repeater ${String(index + 1).padStart(2, '0')}`,
|
||||
@@ -109,7 +97,6 @@ async function installApiFixtures(page: Page) {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (pathname === '/api/health') return route.fulfill({ json: health });
|
||||
if (pathname === '/api/repeaters/firmware') {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
@@ -168,7 +155,6 @@ const routedViews = [
|
||||
['UK install', 'http://127.0.0.1:4173/install'],
|
||||
['UK open source', 'http://127.0.0.1:4173/open-source'],
|
||||
['UK stats', 'http://127.0.0.1:4173/stats'],
|
||||
['UK health', 'http://127.0.0.1:4173/health'],
|
||||
['UK login', 'http://127.0.0.1:4173/login'],
|
||||
['UK docs', 'http://127.0.0.1:4173/docs'],
|
||||
['test site home', 'http://127.0.0.1:4175/'],
|
||||
|
||||
@@ -105,22 +105,6 @@ test('public site exposes its primary journeys', async ({ page }) => {
|
||||
},
|
||||
});
|
||||
});
|
||||
await page.route('**/api/health**', async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
status: 'healthy',
|
||||
generatedAt: '2026-07-11T12:00:00Z',
|
||||
maintenance: { active: false, message: null },
|
||||
incidents: [],
|
||||
components: {
|
||||
ingest: { status: 'ok' },
|
||||
workers: { status: 'running' },
|
||||
storage: { status: 'ok' },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('heading', { name: 'UK Mesh Network' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Open live map' }).first()).toBeVisible();
|
||||
@@ -142,15 +126,7 @@ test('public site exposes its primary journeys', async ({ page }) => {
|
||||
await expect(page.getByLabel('Geographic repeater topology graph')).toBeVisible();
|
||||
await expect(page.getByRole('group', { name: '2 positioned repeaters and 1 links' })).toBeVisible();
|
||||
|
||||
await page.getByRole('link', { name: 'Health', exact: true }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Platform status' })).toBeVisible();
|
||||
await expect(page.getByText('All monitored systems operational')).toBeVisible();
|
||||
await expect(page.getByText('Status data is currently unavailable.')).toHaveCount(0);
|
||||
await expect(page.getByRole('heading', { name: 'Public ingest' })).toBeVisible();
|
||||
const statusAccessibility = await new AxeBuilder({ page }).analyze();
|
||||
expect(statusAccessibility.violations
|
||||
.filter((violation) => violation.impact === 'critical' || violation.impact === 'serious')
|
||||
.map((violation) => ({ id: violation.id, targets: violation.nodes.map((node) => node.target.join(' ')) }))).toEqual([]);
|
||||
await expect(page.getByRole('link', { name: 'Health', exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
// Guards issue #3 (contrast): the feed chrome renders the muted/secondary text
|
||||
|
||||
Reference in New Issue
Block a user