feat(status): redesign status page with hero, themed sections, responsive grid

Rework the public / status page from a flat uniform grid into a hero
featured metric plus four themed sections (Feeds, Emails, Distribution,
Instance). Add semantic colors (green success, red rejects/deletes),
relative timestamps with UTC tooltips, and derived metrics (net feeds,
acceptance rate, avg emails/feed, humanized uptime). Grid is fluid above
640px (auto-fit) and locks to two columns on mobile.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-23 12:39:57 +02:00
parent 81d05e5774
commit b985e2c643
2 changed files with 243 additions and 23 deletions
+149 -22
View File
@@ -7,24 +7,81 @@ function formatDateTime(iso?: string): string {
if (!iso) return "Never";
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return "Never";
return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
return date
.toISOString()
.replace("T", " ")
.replace(/\.\d+Z$/, " UTC");
}
function formatRelative(iso?: string): string {
if (!iso) return "Never";
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return "Never";
const diffMs = Date.now() - date.getTime();
if (diffMs < 0) return "just now";
const seconds = Math.floor(diffMs / 1000);
if (seconds < 60) return "just now";
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
function formatUptime(iso?: string): string {
if (!iso) return "—";
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return "—";
const diffMs = Date.now() - date.getTime();
if (diffMs < 0) return "—";
const minutes = Math.floor(diffMs / 60000);
if (minutes < 60) return `${minutes} min`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours} ${hours === 1 ? "hour" : "hours"}`;
const days = Math.floor(hours / 24);
return `${days} ${days === 1 ? "day" : "days"}`;
}
type Tone = "success" | "danger";
type StatProps = {
label: string;
value: string | number;
tone?: Tone;
title?: string;
time?: boolean;
};
const Stat = ({ label, value, tone, title, time }: StatProps) => {
const valueClass = [
"stat-value",
time ? "stat-value-time" : "",
tone ? `stat-value-${tone}` : "",
]
.filter(Boolean)
.join(" ");
return (
<div class="card stat-card" title={title}>
<span class={valueClass}>{value}</span>
<span class="stat-label">{label}</span>
</div>
);
};
export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
const stats = await getStats(c.env);
const cards: Array<{ label: string; value: string | number; time?: boolean }> =
[
{ label: "Active feeds", value: stats.active_feeds },
{ label: "Feeds created", value: stats.feeds_created },
{ label: "Feeds deleted", value: stats.feeds_deleted },
{ label: "Emails received", value: stats.emails_received },
{ label: "Emails rejected", value: stats.emails_rejected },
{ label: "WebSub subscribers", value: stats.websub_subscriptions_active },
{ label: "Last email", value: formatDateTime(stats.last_email_at), time: true },
{ label: "Last feed created", value: formatDateTime(stats.last_feed_created_at), time: true },
{ label: "Online since", value: formatDateTime(stats.first_seen), time: true },
];
const netFeeds = stats.feeds_created - stats.feeds_deleted;
const totalEmails = stats.emails_received + stats.emails_rejected;
const acceptanceRate =
totalEmails > 0
? `${Math.round((stats.emails_received / totalEmails) * 100)}%`
: "—";
const avgEmailsPerFeed =
stats.feeds_created > 0
? (stats.emails_received / stats.feeds_created).toFixed(1)
: "—";
return c.html(
<Layout title="Status" label="status">
@@ -41,16 +98,86 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
</div>
</div>
<div class="stats-grid">
{cards.map((card) => (
<div class="card stat-card">
<span class={`stat-value${card.time ? " stat-value-time" : ""}`}>
{card.value}
</span>
<span class="stat-label">{card.label}</span>
</div>
))}
<div class="card stat-hero">
<div class="stat-hero-main">
<span class="stat-hero-value">{stats.active_feeds}</span>
<span class="stat-hero-label">Active feeds</span>
</div>
<div class="stat-hero-aside">
<span class="stat-hero-aside-value">{stats.emails_received}</span>
<span class="stat-hero-aside-label">Emails received</span>
</div>
</div>
<section class="stat-section">
<h2 class="stat-section-title">Feeds</h2>
<div class="stats-grid">
<Stat label="Feeds created" value={stats.feeds_created} />
<Stat
label="Feeds deleted"
value={stats.feeds_deleted}
tone="danger"
/>
<Stat label="Net feeds" value={netFeeds} />
<Stat
label="Last feed created"
value={formatRelative(stats.last_feed_created_at)}
title={formatDateTime(stats.last_feed_created_at)}
time
/>
</div>
</section>
<section class="stat-section">
<h2 class="stat-section-title">Emails</h2>
<div class="stats-grid">
<Stat
label="Emails received"
value={stats.emails_received}
tone="success"
/>
<Stat
label="Emails rejected"
value={stats.emails_rejected}
tone="danger"
/>
<Stat
label="Acceptance rate"
value={acceptanceRate}
tone="success"
/>
<Stat label="Avg emails / feed" value={avgEmailsPerFeed} />
<Stat
label="Last email"
value={formatRelative(stats.last_email_at)}
title={formatDateTime(stats.last_email_at)}
time
/>
</div>
</section>
<section class="stat-section">
<h2 class="stat-section-title">Distribution</h2>
<div class="stats-grid">
<Stat
label="WebSub subscribers"
value={stats.websub_subscriptions_active}
/>
</div>
</section>
<section class="stat-section">
<h2 class="stat-section-title">Instance</h2>
<div class="stats-grid">
<Stat label="Uptime" value={formatUptime(stats.first_seen)} time />
<Stat
label="Online since"
value={formatRelative(stats.first_seen)}
title={formatDateTime(stats.first_seen)}
time
/>
</div>
</section>
</div>
</Layout>,
);