-
Signal
-
signal-cli status and channel configuration.
- ${accountCountLabel}
-
-
-
- Configured
- ${configured == null ? "n/a" : configured ? "Yes" : "No"}
-
-
- Running
- ${signal?.running ? "Yes" : "No"}
-
-
- Base URL
- ${signal?.baseUrl ?? "n/a"}
-
-
- Last start
- ${signal?.lastStartAt ? formatRelativeTimestamp(signal.lastStartAt) : "n/a"}
-
-
- Last probe
- ${signal?.lastProbeAt ? formatRelativeTimestamp(signal.lastProbeAt) : "n/a"}
-
-
-
- ${
- signal?.lastError
- ? html`
- ${signal.lastError}
-
`
- : nothing
- }
-
- ${
- signal?.probe
- ? html`
- Probe ${signal.probe.ok ? "ok" : "failed"} ·
- ${signal.probe.status ?? ""} ${signal.probe.error ?? ""}
-
`
- : nothing
- }
-
- ${renderChannelConfigSection({ channelId: "signal", props })}
-
-
-
-
-
- `;
+ return renderSingleAccountChannelCard({
+ title: "Signal",
+ subtitle: "signal-cli status and channel configuration.",
+ accountCountLabel,
+ statusRows: [
+ { label: "Configured", value: formatNullableBoolean(configured) },
+ { label: "Running", value: signal?.running ? "Yes" : "No" },
+ { label: "Base URL", value: signal?.baseUrl ?? "n/a" },
+ {
+ label: "Last start",
+ value: signal?.lastStartAt ? formatRelativeTimestamp(signal.lastStartAt) : "n/a",
+ },
+ {
+ label: "Last probe",
+ value: signal?.lastProbeAt ? formatRelativeTimestamp(signal.lastProbeAt) : "n/a",
+ },
+ ],
+ lastError: signal?.lastError,
+ secondaryCallout: signal?.probe
+ ? html`
+ Probe ${signal.probe.ok ? "ok" : "failed"} ·
+ ${signal.probe.status ?? ""} ${signal.probe.error ?? ""}
+
`
+ : nothing,
+ configSection: renderChannelConfigSection({ channelId: "signal", props }),
+ footer: html`
-
Slack
-
Socket mode status and channel configuration.
- ${accountCountLabel}
-
-
-
- Configured
- ${configured == null ? "n/a" : configured ? "Yes" : "No"}
-
-
- Running
- ${slack?.running ? "Yes" : "No"}
-
-
- Last start
- ${slack?.lastStartAt ? formatRelativeTimestamp(slack.lastStartAt) : "n/a"}
-
-
- Last probe
- ${slack?.lastProbeAt ? formatRelativeTimestamp(slack.lastProbeAt) : "n/a"}
-
-
-
- ${
- slack?.lastError
- ? html`
- ${slack.lastError}
-
`
- : nothing
- }
-
- ${
- slack?.probe
- ? html`
- Probe ${slack.probe.ok ? "ok" : "failed"} ·
- ${slack.probe.status ?? ""} ${slack.probe.error ?? ""}
-
`
- : nothing
- }
-
- ${renderChannelConfigSection({ channelId: "slack", props })}
-
-
-
-
-
- `;
+ return renderSingleAccountChannelCard({
+ title: "Slack",
+ subtitle: "Socket mode status and channel configuration.",
+ accountCountLabel,
+ statusRows: [
+ { label: "Configured", value: formatNullableBoolean(configured) },
+ { label: "Running", value: slack?.running ? "Yes" : "No" },
+ {
+ label: "Last start",
+ value: slack?.lastStartAt ? formatRelativeTimestamp(slack.lastStartAt) : "n/a",
+ },
+ {
+ label: "Last probe",
+ value: slack?.lastProbeAt ? formatRelativeTimestamp(slack.lastProbeAt) : "n/a",
+ },
+ ],
+ lastError: slack?.lastError,
+ secondaryCallout: slack?.probe
+ ? html`
+ Probe ${slack.probe.ok ? "ok" : "failed"} ·
+ ${slack.probe.status ?? ""} ${slack.probe.error ?? ""}
+
`
+ : nothing,
+ configSection: renderChannelConfigSection({ channelId: "slack", props }),
+ footer: html`
-
Telegram
-
Bot status and channel configuration.
- ${accountCountLabel}
+ if (hasMultipleAccounts) {
+ return html`
+
+
Telegram
+
Bot status and channel configuration.
+ ${accountCountLabel}
- ${
- hasMultipleAccounts
- ? html`
-
- ${telegramAccounts.map((account) => renderAccountCard(account))}
-
- `
- : html`
-
-
- Configured
- ${configured == null ? "n/a" : configured ? "Yes" : "No"}
-
-
- Running
- ${telegram?.running ? "Yes" : "No"}
-
-
- Mode
- ${telegram?.mode ?? "n/a"}
-
-
- Last start
- ${telegram?.lastStartAt ? formatRelativeTimestamp(telegram.lastStartAt) : "n/a"}
-
-
- Last probe
- ${telegram?.lastProbeAt ? formatRelativeTimestamp(telegram.lastProbeAt) : "n/a"}
-
-
- `
- }
+
+ ${telegramAccounts.map((account) => renderAccountCard(account))}
+
- ${
- telegram?.lastError
- ? html`
- ${telegram.lastError}
-
`
- : nothing
- }
+ ${
+ telegram?.lastError
+ ? html`
+ ${telegram.lastError}
+
`
+ : nothing
+ }
- ${
- telegram?.probe
- ? html`
- Probe ${telegram.probe.ok ? "ok" : "failed"} ·
- ${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
-
`
- : nothing
- }
+ ${
+ telegram?.probe
+ ? html`
+ Probe ${telegram.probe.ok ? "ok" : "failed"} ·
+ ${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
+
`
+ : nothing
+ }
- ${renderChannelConfigSection({ channelId: "telegram", props })}
+ ${renderChannelConfigSection({ channelId: "telegram", props })}
-
-
+
+
+
-
- `;
+ `;
+ }
+
+ return renderSingleAccountChannelCard({
+ title: "Telegram",
+ subtitle: "Bot status and channel configuration.",
+ accountCountLabel,
+ statusRows: [
+ { label: "Configured", value: formatNullableBoolean(configured) },
+ { label: "Running", value: telegram?.running ? "Yes" : "No" },
+ { label: "Mode", value: telegram?.mode ?? "n/a" },
+ {
+ label: "Last start",
+ value: telegram?.lastStartAt ? formatRelativeTimestamp(telegram.lastStartAt) : "n/a",
+ },
+ {
+ label: "Last probe",
+ value: telegram?.lastProbeAt ? formatRelativeTimestamp(telegram.lastProbeAt) : "n/a",
+ },
+ ],
+ lastError: telegram?.lastError,
+ secondaryCallout: telegram?.probe
+ ? html`
+ Probe ${telegram.probe.ok ? "ok" : "failed"} ·
+ ${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
+
`
+ : nothing,
+ configSection: renderChannelConfigSection({ channelId: "telegram", props }),
+ footer: html`
+
+
`,
+ });
}
diff --git a/ui/src/ui/views/channels.shared.test.ts b/ui/src/ui/views/channels.test.ts
similarity index 66%
rename from ui/src/ui/views/channels.shared.test.ts
rename to ui/src/ui/views/channels.test.ts
index ce879c147b3..c60cf989dbf 100644
--- a/ui/src/ui/views/channels.shared.test.ts
+++ b/ui/src/ui/views/channels.test.ts
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
-import { resolveChannelConfigured } from "./channels.shared.ts";
+import {
+ channelEnabled,
+ resolveChannelConfigured,
+ resolveChannelDisplayState,
+} from "./channels.shared.ts";
import type { ChannelsProps } from "./channels.types.ts";
function createProps(snapshot: ChannelsProps["snapshot"]): ChannelsProps {
@@ -37,7 +41,7 @@ function createProps(snapshot: ChannelsProps["snapshot"]): ChannelsProps {
};
}
-describe("resolveChannelConfigured", () => {
+describe("channel display selectors", () => {
it("returns the channel summary configured flag when present", () => {
const props = createProps({
ts: Date.now(),
@@ -51,6 +55,7 @@ describe("resolveChannelConfigured", () => {
});
expect(resolveChannelConfigured("discord", props)).toBe(false);
+ expect(resolveChannelDisplayState("discord", props).configured).toBe(false);
});
it("falls back to the default account when the channel summary omits configured", () => {
@@ -68,7 +73,11 @@ describe("resolveChannelConfigured", () => {
channelDefaultAccountId: { discord: "discord-main" },
});
+ const displayState = resolveChannelDisplayState("discord", props);
+
expect(resolveChannelConfigured("discord", props)).toBe(true);
+ expect(displayState.defaultAccount?.accountId).toBe("discord-main");
+ expect(channelEnabled("discord", props)).toBe(true);
});
it("falls back to the first account when no default account id is available", () => {
@@ -83,6 +92,29 @@ describe("resolveChannelConfigured", () => {
channelDefaultAccountId: {},
});
+ const displayState = resolveChannelDisplayState("slack", props);
+
expect(resolveChannelConfigured("slack", props)).toBe(true);
+ expect(displayState.defaultAccount?.accountId).toBe("workspace-a");
+ });
+
+ it("keeps disabled channels hidden when neither summary nor accounts are active", () => {
+ const props = createProps({
+ ts: Date.now(),
+ channelOrder: ["signal"],
+ channelLabels: { signal: "Signal" },
+ channels: { signal: {} },
+ channelAccounts: {
+ signal: [{ accountId: "default", configured: false, running: false, connected: false }],
+ },
+ channelDefaultAccountId: { signal: "default" },
+ });
+
+ const displayState = resolveChannelDisplayState("signal", props);
+
+ expect(displayState.configured).toBe(false);
+ expect(displayState.running).toBeNull();
+ expect(displayState.connected).toBeNull();
+ expect(channelEnabled("signal", props)).toBe(false);
});
});
diff --git a/ui/src/ui/views/channels.ts b/ui/src/ui/views/channels.ts
index 8b6ee7cdd9d..f45bc9e9486 100644
--- a/ui/src/ui/views/channels.ts
+++ b/ui/src/ui/views/channels.ts
@@ -21,8 +21,9 @@ import { renderIMessageCard } from "./channels.imessage.ts";
import { renderNostrCard } from "./channels.nostr.ts";
import {
channelEnabled,
+ formatNullableBoolean,
renderChannelAccountCount,
- resolveChannelConfigured,
+ resolveChannelDisplayState,
} from "./channels.shared.ts";
import { renderSignalCard } from "./channels.signal.ts";
import { renderSlackCard } from "./channels.slack.ts";
@@ -187,11 +188,9 @@ function renderGenericChannelCard(
channelAccounts: Record
,
) {
const label = resolveChannelLabel(props.snapshot, key);
- const status = props.snapshot?.channels?.[key] as Record | undefined;
- const configured = resolveChannelConfigured(key, props);
- const running = typeof status?.running === "boolean" ? status.running : undefined;
- const connected = typeof status?.connected === "boolean" ? status.connected : undefined;
- const lastError = typeof status?.lastError === "string" ? status.lastError : undefined;
+ const displayState = resolveChannelDisplayState(key, props);
+ const lastError =
+ typeof displayState.status?.lastError === "string" ? displayState.status.lastError : undefined;
const accounts = channelAccounts[key] ?? [];
const accountCountLabel = renderChannelAccountCount(key, channelAccounts);
@@ -212,15 +211,15 @@ function renderGenericChannelCard(
Configured
- ${configured == null ? "n/a" : configured ? "Yes" : "No"}
+ ${formatNullableBoolean(displayState.configured)}
Running
- ${running == null ? "n/a" : running ? "Yes" : "No"}
+ ${formatNullableBoolean(displayState.running)}
Connected
- ${connected == null ? "n/a" : connected ? "Yes" : "No"}
+ ${formatNullableBoolean(displayState.connected)}
`
diff --git a/ui/src/ui/views/channels.whatsapp.ts b/ui/src/ui/views/channels.whatsapp.ts
index d6e0a90e1e8..06c2b4d8484 100644
--- a/ui/src/ui/views/channels.whatsapp.ts
+++ b/ui/src/ui/views/channels.whatsapp.ts
@@ -2,7 +2,11 @@ import { html, nothing } from "lit";
import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts";
import type { WhatsAppStatus } from "../types.ts";
import { renderChannelConfigSection } from "./channels.config.ts";
-import { resolveChannelConfigured } from "./channels.shared.ts";
+import {
+ formatNullableBoolean,
+ renderSingleAccountChannelCard,
+ resolveChannelConfigured,
+} from "./channels.shared.ts";
import type { ChannelsProps } from "./channels.types.ts";
export function renderWhatsAppCard(params: {
@@ -13,108 +17,81 @@ export function renderWhatsAppCard(params: {
const { props, whatsapp, accountCountLabel } = params;
const configured = resolveChannelConfigured("whatsapp", props);
- return html`
-
-
WhatsApp
-
Link WhatsApp Web and monitor connection health.
- ${accountCountLabel}
-
-
-
- Configured
- ${configured == null ? "n/a" : configured ? "Yes" : "No"}
-
-
- Linked
- ${whatsapp?.linked ? "Yes" : "No"}
-
-
- Running
- ${whatsapp?.running ? "Yes" : "No"}
-
-
- Connected
- ${whatsapp?.connected ? "Yes" : "No"}
-
-
- Last connect
-
- ${whatsapp?.lastConnectedAt ? formatRelativeTimestamp(whatsapp.lastConnectedAt) : "n/a"}
-
-
-
- Last message
-
- ${whatsapp?.lastMessageAt ? formatRelativeTimestamp(whatsapp.lastMessageAt) : "n/a"}
-
-
-
- Auth age
-
- ${whatsapp?.authAgeMs != null ? formatDurationHuman(whatsapp.authAgeMs) : "n/a"}
-
-
-
-
- ${
- whatsapp?.lastError
- ? html`
- ${whatsapp.lastError}
-
`
- : nothing
- }
-
+ return renderSingleAccountChannelCard({
+ title: "WhatsApp",
+ subtitle: "Link WhatsApp Web and monitor connection health.",
+ accountCountLabel,
+ statusRows: [
+ { label: "Configured", value: formatNullableBoolean(configured) },
+ { label: "Linked", value: whatsapp?.linked ? "Yes" : "No" },
+ { label: "Running", value: whatsapp?.running ? "Yes" : "No" },
+ { label: "Connected", value: whatsapp?.connected ? "Yes" : "No" },
+ {
+ label: "Last connect",
+ value: whatsapp?.lastConnectedAt
+ ? formatRelativeTimestamp(whatsapp.lastConnectedAt)
+ : "n/a",
+ },
+ {
+ label: "Last message",
+ value: whatsapp?.lastMessageAt ? formatRelativeTimestamp(whatsapp.lastMessageAt) : "n/a",
+ },
+ {
+ label: "Auth age",
+ value: whatsapp?.authAgeMs != null ? formatDurationHuman(whatsapp.authAgeMs) : "n/a",
+ },
+ ],
+ lastError: whatsapp?.lastError,
+ extraContent: html`
${
props.whatsappMessage
? html`
- ${props.whatsappMessage}
-
`
+ ${props.whatsappMessage}
+
`
: nothing
}
${
props.whatsappQrDataUrl
? html`
-

-
`
+
+ `
: nothing
}
-
-