Imports the visual direction from a claude.ai/design session ("PulseNode
Dashboard.dc.html") and reconciles it with the app's existing choices
rather than a wholesale swap: kept the IBM Plex Sans/Mono + Space
Grotesk typography and the pulse-green accent concept from the earlier
pass, adopted the imported design's structural ideas - a real
tonal/elevation token system (derived divider/muted colors via
color-mix instead of hardcoded per-theme duplicates, actual box-shadow
elevation), a 12-column bento grid with per-widget-type column spans
instead of uniform equal-width cards, fading-edge section dividers,
and a shared WidgetCard/MetricBar/StatusTag vocabulary so every widget
type stops repeating its own card markup.
Icons come from @phosphor-icons/react's /ssr entrypoint (bundled at
build time) rather than the imported design's unpkg.com CDN script -
that script is fine for the standalone design-tool preview, but a
runtime third-party call would break the self-hosted-only principle
already established (fonts self-hosted via next/font, no external
requests). New app/icon.svg reuses the same nav badge mark as the
favicon, replacing the never-touched create-next-app default.
Two new pieces of real functionality prompted by the design's mockup
toast/live-indicator, not just decoration: a "live" WebSocket
connection-status indicator (lib/ws/client.ts now tracks and exposes
real connection state), and a toast that fires on actual config:update
and config:error events - the latter finally surfaces config validation
failures in the browser, previously visible only in server logs despite
being designed for exactly this back in M1.
46 lines
1.9 KiB
TypeScript
46 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import { Globe, Timer } from "@phosphor-icons/react/ssr";
|
|
import type { Widget } from "@/lib/config/schema";
|
|
import { useWidgetSubscription } from "@/lib/ws/client";
|
|
import { SkeletonLines } from "@/components/widgets/Skeleton";
|
|
import { WidgetCard } from "@/components/widgets/WidgetCard";
|
|
import { StatusTag } from "@/components/widgets/StatusTag";
|
|
|
|
type HttpWidget = Extract<Widget, { type: "http" }>;
|
|
|
|
function statusTone(up: boolean | undefined, consecutiveFailures: number): "accent" | "down" | "degraded" | "neutral" {
|
|
if (up === undefined) return "neutral";
|
|
if (up) return "accent";
|
|
return consecutiveFailures > 2 ? "down" : "degraded";
|
|
}
|
|
|
|
export function HttpWidget({ widget, widgetId }: { widget: HttpWidget; widgetId: string }) {
|
|
const result = useWidgetSubscription(widgetId);
|
|
const data = result?.type === "http" ? result.data : null;
|
|
const errorMessage = result?.type === "error" ? result.message : null;
|
|
|
|
return (
|
|
<WidgetCard span={3}>
|
|
<div className="flex items-center gap-2.5">
|
|
<Globe size={18} className="text-accent" aria-hidden />
|
|
<span className="flex-1 text-[15px] font-medium text-fg">{widget.name}</span>
|
|
</div>
|
|
<div>
|
|
<StatusTag tone={statusTone(data?.up, data?.consecutiveFailures ?? 0)}>
|
|
{data ? (data.up ? `${data.statusCode ?? "up"}` : "down") : "…"}
|
|
</StatusTag>
|
|
</div>
|
|
{errorMessage && <span className="text-xs text-status-down">{errorMessage}</span>}
|
|
{!result && !errorMessage && <SkeletonLines />}
|
|
{data && (
|
|
<div className="flex items-center gap-1.5 text-[11px] text-fg-muted">
|
|
<Timer size={12} aria-hidden />
|
|
<span className="font-mono tabular-nums text-fg">{data.latencyMs}ms</span> latency
|
|
{!data.up && data.error && <span className="text-status-down">{data.error}</span>}
|
|
</div>
|
|
)}
|
|
</WidgetCard>
|
|
);
|
|
}
|