feat: initial Signage 0.1.0

This commit is contained in:
ekklesia
2026-08-28 16:41:54 -05:00
parent 20756f11af
commit 92bef4c7aa
107 changed files with 24397 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
import type { ModuleUiHostContext } from "@ekklesia/module-ui-sdk";
import {
Badge,
EmptyState,
Panel,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@ekklesia/ui";
import type { ReactElement } from "react";
import type { SignageApi } from "../api.js";
import { useResource } from "../hooks.js";
import type { ProviderHealth, SettingsResponse } from "../types.js";
import { formatInstant, renderLoadable } from "./shared.js";
const SLIDE_RENDERER_CAPABILITY = "signage.slide-renderer";
/**
* Provider health, and the one thing only a browser can tell an operator.
*
* A registration is a **running fact**: a provider that was uninstalled or disabled simply is not
* here, and there is no stored row claiming otherwise. Everything shown is either the provider's own
* declared identity or a count Signage kept — never a provider's exception, message, or payload.
*
* Renderer availability is checked here rather than on the server because the server genuinely
* cannot know it: whether the platform published a module's contribution is a fact the *host* holds,
* and `context.contributions` is where it is legible. A provider whose renderer was not published —
* because the module is missing, disabled, or outside Signage's compatible range — is reported as
* exactly that, which is the difference between "this is broken" and "this needs an update".
*/
export function ProvidersPanel({
api,
context,
settings,
}: {
readonly api: SignageApi;
readonly context: ModuleUiHostContext;
readonly settings: SettingsResponse;
}): ReactElement {
const providers = useResource(
() => api.get<{ providers: readonly ProviderHealth[] }>("providers"),
[],
);
const published = new Set(
context.contributions.list(SLIDE_RENDERER_CAPABILITY).map((contribution) => contribution.id),
);
return (
<Panel
description={`Renderer input contract ${settings.rendererInputVersion}. Only providers that are actually running appear here.`}
title="Content providers"
>
{renderLoadable(
providers.state,
(value) =>
value.providers.length === 0 ? (
<EmptyState
description="Signage works on its own with native images. Install a content module to add more."
title="No content providers registered"
/>
) : (
<Table label="Provider health">
<TableHead>
<TableRow>
<TableCell header>Provider</TableCell>
<TableCell header>Contract</TableCell>
<TableCell header>Renderer</TableCell>
<TableCell header>Health</TableCell>
<TableCell header>Last success</TableCell>
</TableRow>
</TableHead>
<TableBody>
{value.providers.map((provider) => {
const rendererPublished = published.has(provider.renderer.contributionId);
const isNative = provider.renderer.contributionId === "signage.native-image";
return (
<TableRow key={provider.providerId}>
<TableCell>
<span className="signage-provider__id">{provider.providerId}</span>
<span className="signage-provider__version">{`v${provider.providerVersion}`}</span>
</TableCell>
<TableCell>{provider.contractVersion}</TableCell>
<TableCell>
{isNative ? (
<Badge tone="success">Signage renders this</Badge>
) : rendererPublished ? (
<Badge tone="success">{provider.renderer.contributionId}</Badge>
) : (
<Badge tone="warning">Renderer not published</Badge>
)}
</TableCell>
<TableCell>
{provider.consecutiveFailures === 0 ? (
<Badge tone="success">Healthy</Badge>
) : (
<Badge tone="danger">
{`${provider.consecutiveFailures} failures · ${provider.lastFailureCode ?? "unknown"}`}
</Badge>
)}
</TableCell>
<TableCell>
{formatInstant(provider.lastSuccessAt, settings.timezone)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
),
{ label: "Loading providers…", onRetry: providers.reload },
)}
</Panel>
);
}