mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
refactor: share channel card selectors and layout
This commit is contained in:
@@ -5,6 +5,7 @@ describe("isUnitConfigTestFile", () => {
|
||||
it("accepts unit-config src, test, and whitelisted ui tests", () => {
|
||||
expect(isUnitConfigTestFile("src/infra/git-commit.test.ts")).toBe(true);
|
||||
expect(isUnitConfigTestFile("test/format-error.test.ts")).toBe(true);
|
||||
expect(isUnitConfigTestFile("ui/src/ui/views/channels.test.ts")).toBe(true);
|
||||
expect(isUnitConfigTestFile("ui/src/ui/views/chat.test.ts")).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ import { html, nothing } from "lit";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import type { DiscordStatus } 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 renderDiscordCard(params: {
|
||||
@@ -13,55 +17,34 @@ export function renderDiscordCard(params: {
|
||||
const { props, discord, accountCountLabel } = params;
|
||||
const configured = resolveChannelConfigured("discord", props);
|
||||
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">Discord</div>
|
||||
<div class="card-sub">Bot status and channel configuration.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Running</span>
|
||||
<span>${discord?.running ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${discord?.lastStartAt ? formatRelativeTimestamp(discord.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${discord?.lastProbeAt ? formatRelativeTimestamp(discord.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
discord?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${discord.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
discord?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${discord.probe.ok ? "ok" : "failed"} ·
|
||||
${discord.probe.status ?? ""} ${discord.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "discord", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return renderSingleAccountChannelCard({
|
||||
title: "Discord",
|
||||
subtitle: "Bot status and channel configuration.",
|
||||
accountCountLabel,
|
||||
statusRows: [
|
||||
{ label: "Configured", value: formatNullableBoolean(configured) },
|
||||
{ label: "Running", value: discord?.running ? "Yes" : "No" },
|
||||
{
|
||||
label: "Last start",
|
||||
value: discord?.lastStartAt ? formatRelativeTimestamp(discord.lastStartAt) : "n/a",
|
||||
},
|
||||
{
|
||||
label: "Last probe",
|
||||
value: discord?.lastProbeAt ? formatRelativeTimestamp(discord.lastProbeAt) : "n/a",
|
||||
},
|
||||
],
|
||||
lastError: discord?.lastError,
|
||||
secondaryCallout: discord?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${discord.probe.ok ? "ok" : "failed"} ·
|
||||
${discord.probe.status ?? ""} ${discord.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "discord", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { html, nothing } from "lit";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import type { GoogleChatStatus } 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 renderGoogleChatCard(params: {
|
||||
@@ -13,69 +17,44 @@ export function renderGoogleChatCard(params: {
|
||||
const { props, googleChat, accountCountLabel } = params;
|
||||
const configured = resolveChannelConfigured("googlechat", props);
|
||||
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">Google Chat</div>
|
||||
<div class="card-sub">Chat API webhook status and channel configuration.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Running</span>
|
||||
<span>${googleChat ? (googleChat.running ? "Yes" : "No") : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Credential</span>
|
||||
<span>${googleChat?.credentialSource ?? "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Audience</span>
|
||||
<span>
|
||||
${
|
||||
googleChat?.audienceType
|
||||
? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}`
|
||||
: "n/a"
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${googleChat?.lastStartAt ? formatRelativeTimestamp(googleChat.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${googleChat?.lastProbeAt ? formatRelativeTimestamp(googleChat.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
googleChat?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${googleChat.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
googleChat?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${googleChat.probe.ok ? "ok" : "failed"} ·
|
||||
${googleChat.probe.status ?? ""} ${googleChat.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "googlechat", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return renderSingleAccountChannelCard({
|
||||
title: "Google Chat",
|
||||
subtitle: "Chat API webhook status and channel configuration.",
|
||||
accountCountLabel,
|
||||
statusRows: [
|
||||
{ label: "Configured", value: formatNullableBoolean(configured) },
|
||||
{
|
||||
label: "Running",
|
||||
value: googleChat ? (googleChat.running ? "Yes" : "No") : "n/a",
|
||||
},
|
||||
{ label: "Credential", value: googleChat?.credentialSource ?? "n/a" },
|
||||
{
|
||||
label: "Audience",
|
||||
value: googleChat?.audienceType
|
||||
? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}`
|
||||
: "n/a",
|
||||
},
|
||||
{
|
||||
label: "Last start",
|
||||
value: googleChat?.lastStartAt ? formatRelativeTimestamp(googleChat.lastStartAt) : "n/a",
|
||||
},
|
||||
{
|
||||
label: "Last probe",
|
||||
value: googleChat?.lastProbeAt ? formatRelativeTimestamp(googleChat.lastProbeAt) : "n/a",
|
||||
},
|
||||
],
|
||||
lastError: googleChat?.lastError,
|
||||
secondaryCallout: googleChat?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${googleChat.probe.ok ? "ok" : "failed"} ·
|
||||
${googleChat.probe.status ?? ""} ${googleChat.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "googlechat", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { html, nothing } from "lit";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import type { IMessageStatus } 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 renderIMessageCard(params: {
|
||||
@@ -13,55 +17,34 @@ export function renderIMessageCard(params: {
|
||||
const { props, imessage, accountCountLabel } = params;
|
||||
const configured = resolveChannelConfigured("imessage", props);
|
||||
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">iMessage</div>
|
||||
<div class="card-sub">macOS bridge status and channel configuration.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Running</span>
|
||||
<span>${imessage?.running ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${imessage?.lastStartAt ? formatRelativeTimestamp(imessage.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${imessage?.lastProbeAt ? formatRelativeTimestamp(imessage.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
imessage?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${imessage.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
imessage?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${imessage.probe.ok ? "ok" : "failed"} ·
|
||||
${imessage.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "imessage", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return renderSingleAccountChannelCard({
|
||||
title: "iMessage",
|
||||
subtitle: "macOS bridge status and channel configuration.",
|
||||
accountCountLabel,
|
||||
statusRows: [
|
||||
{ label: "Configured", value: formatNullableBoolean(configured) },
|
||||
{ label: "Running", value: imessage?.running ? "Yes" : "No" },
|
||||
{
|
||||
label: "Last start",
|
||||
value: imessage?.lastStartAt ? formatRelativeTimestamp(imessage.lastStartAt) : "n/a",
|
||||
},
|
||||
{
|
||||
label: "Last probe",
|
||||
value: imessage?.lastProbeAt ? formatRelativeTimestamp(imessage.lastProbeAt) : "n/a",
|
||||
},
|
||||
],
|
||||
lastError: imessage?.lastError,
|
||||
secondaryCallout: imessage?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${imessage.probe.ok ? "ok" : "failed"} ·
|
||||
${imessage.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "imessage", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,42 +2,138 @@ import { html, nothing } from "lit";
|
||||
import type { ChannelAccountSnapshot } from "../types.ts";
|
||||
import type { ChannelKey, ChannelsProps } from "./channels.types.ts";
|
||||
|
||||
export function channelEnabled(key: ChannelKey, props: ChannelsProps) {
|
||||
const snapshot = props.snapshot;
|
||||
const channels = snapshot?.channels as Record<string, unknown> | null;
|
||||
if (!snapshot || !channels) {
|
||||
return false;
|
||||
}
|
||||
const channelStatus = channels[key] as Record<string, unknown> | undefined;
|
||||
const configured = typeof channelStatus?.configured === "boolean" && channelStatus.configured;
|
||||
const running = typeof channelStatus?.running === "boolean" && channelStatus.running;
|
||||
const connected = typeof channelStatus?.connected === "boolean" && channelStatus.connected;
|
||||
const accounts = snapshot.channelAccounts?.[key] ?? [];
|
||||
const accountActive = accounts.some(
|
||||
type ChannelDisplayState = {
|
||||
configured: boolean | null;
|
||||
running: boolean | null;
|
||||
connected: boolean | null;
|
||||
defaultAccount: ChannelAccountSnapshot | null;
|
||||
hasAnyActiveAccount: boolean;
|
||||
status: Record<string, unknown> | undefined;
|
||||
};
|
||||
|
||||
type ChannelStatusRow = {
|
||||
label: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
function resolveChannelStatus(
|
||||
key: ChannelKey,
|
||||
props: ChannelsProps,
|
||||
): Record<string, unknown> | undefined {
|
||||
const channels = props.snapshot?.channels as Record<string, unknown> | null;
|
||||
return channels?.[key] as Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
export function resolveDefaultChannelAccount(
|
||||
key: ChannelKey,
|
||||
props: ChannelsProps,
|
||||
): ChannelAccountSnapshot | null {
|
||||
const accounts = props.snapshot?.channelAccounts?.[key] ?? [];
|
||||
const defaultAccountId = props.snapshot?.channelDefaultAccountId?.[key];
|
||||
return (
|
||||
(defaultAccountId
|
||||
? accounts.find((account) => account.accountId === defaultAccountId)
|
||||
: undefined) ??
|
||||
accounts[0] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveChannelDisplayState(
|
||||
key: ChannelKey,
|
||||
props: ChannelsProps,
|
||||
): ChannelDisplayState {
|
||||
const status = resolveChannelStatus(key, props);
|
||||
const accounts = props.snapshot?.channelAccounts?.[key] ?? [];
|
||||
const defaultAccount = resolveDefaultChannelAccount(key, props);
|
||||
const configured =
|
||||
typeof status?.configured === "boolean"
|
||||
? status.configured
|
||||
: typeof defaultAccount?.configured === "boolean"
|
||||
? defaultAccount.configured
|
||||
: null;
|
||||
const running = typeof status?.running === "boolean" ? status.running : null;
|
||||
const connected = typeof status?.connected === "boolean" ? status.connected : null;
|
||||
const hasAnyActiveAccount = accounts.some(
|
||||
(account) => account.configured || account.running || account.connected,
|
||||
);
|
||||
return configured || running || connected || accountActive;
|
||||
|
||||
return {
|
||||
configured,
|
||||
running,
|
||||
connected,
|
||||
defaultAccount,
|
||||
hasAnyActiveAccount,
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
export function channelEnabled(key: ChannelKey, props: ChannelsProps) {
|
||||
if (!props.snapshot) {
|
||||
return false;
|
||||
}
|
||||
const displayState = resolveChannelDisplayState(key, props);
|
||||
return (
|
||||
displayState.configured === true ||
|
||||
displayState.running === true ||
|
||||
displayState.connected === true ||
|
||||
displayState.hasAnyActiveAccount
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveChannelConfigured(key: ChannelKey, props: ChannelsProps): boolean | null {
|
||||
const snapshot = props.snapshot;
|
||||
const channels = snapshot?.channels as Record<string, unknown> | null;
|
||||
const channelStatus = channels?.[key] as Record<string, unknown> | undefined;
|
||||
if (typeof channelStatus?.configured === "boolean") {
|
||||
return channelStatus.configured;
|
||||
return resolveChannelDisplayState(key, props).configured;
|
||||
}
|
||||
|
||||
export function formatNullableBoolean(value: boolean | null): string {
|
||||
if (value == null) {
|
||||
return "n/a";
|
||||
}
|
||||
return value ? "Yes" : "No";
|
||||
}
|
||||
|
||||
const accounts = snapshot?.channelAccounts?.[key] ?? [];
|
||||
const defaultAccountId = snapshot?.channelDefaultAccountId?.[key];
|
||||
const defaultAccount = defaultAccountId
|
||||
? accounts.find((account) => account.accountId === defaultAccountId)
|
||||
: accounts[0];
|
||||
export function renderSingleAccountChannelCard(params: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
accountCountLabel: unknown;
|
||||
statusRows: readonly ChannelStatusRow[];
|
||||
lastError?: string | null;
|
||||
secondaryCallout?: unknown;
|
||||
extraContent?: unknown;
|
||||
configSection: unknown;
|
||||
footer?: unknown;
|
||||
}) {
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">${params.title}</div>
|
||||
<div class="card-sub">${params.subtitle}</div>
|
||||
${params.accountCountLabel}
|
||||
|
||||
if (typeof defaultAccount?.configured === "boolean") {
|
||||
return defaultAccount.configured;
|
||||
}
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
${params.statusRows.map(
|
||||
(row) => html`
|
||||
<div>
|
||||
<span class="label">${row.label}</span>
|
||||
<span>${row.value}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
|
||||
return null;
|
||||
${
|
||||
params.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${params.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${params.secondaryCallout ?? nothing}
|
||||
${params.extraContent ?? nothing}
|
||||
${params.configSection}
|
||||
${params.footer ?? nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function getChannelAccountCount(
|
||||
|
||||
@@ -2,7 +2,11 @@ import { html, nothing } from "lit";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import type { SignalStatus } 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 renderSignalCard(params: {
|
||||
@@ -13,59 +17,35 @@ export function renderSignalCard(params: {
|
||||
const { props, signal, accountCountLabel } = params;
|
||||
const configured = resolveChannelConfigured("signal", props);
|
||||
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">Signal</div>
|
||||
<div class="card-sub">signal-cli status and channel configuration.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Running</span>
|
||||
<span>${signal?.running ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Base URL</span>
|
||||
<span>${signal?.baseUrl ?? "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${signal?.lastStartAt ? formatRelativeTimestamp(signal.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${signal?.lastProbeAt ? formatRelativeTimestamp(signal.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
signal?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${signal.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
signal?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${signal.probe.ok ? "ok" : "failed"} ·
|
||||
${signal.probe.status ?? ""} ${signal.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "signal", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${signal.probe.ok ? "ok" : "failed"} ·
|
||||
${signal.probe.status ?? ""} ${signal.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "signal", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { html, nothing } from "lit";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import type { SlackStatus } 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 renderSlackCard(params: {
|
||||
@@ -13,55 +17,34 @@ export function renderSlackCard(params: {
|
||||
const { props, slack, accountCountLabel } = params;
|
||||
const configured = resolveChannelConfigured("slack", props);
|
||||
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">Slack</div>
|
||||
<div class="card-sub">Socket mode status and channel configuration.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Running</span>
|
||||
<span>${slack?.running ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${slack?.lastStartAt ? formatRelativeTimestamp(slack.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${slack?.lastProbeAt ? formatRelativeTimestamp(slack.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
slack?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${slack.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
slack?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${slack.probe.ok ? "ok" : "failed"} ·
|
||||
${slack.probe.status ?? ""} ${slack.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "slack", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${slack.probe.ok ? "ok" : "failed"} ·
|
||||
${slack.probe.status ?? ""} ${slack.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "slack", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { html, nothing } from "lit";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import type { ChannelAccountSnapshot, TelegramStatus } 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 renderTelegramCard(params: {
|
||||
@@ -54,69 +58,74 @@ export function renderTelegramCard(params: {
|
||||
`;
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">Telegram</div>
|
||||
<div class="card-sub">Bot status and channel configuration.</div>
|
||||
${accountCountLabel}
|
||||
if (hasMultipleAccounts) {
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">Telegram</div>
|
||||
<div class="card-sub">Bot status and channel configuration.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
${
|
||||
hasMultipleAccounts
|
||||
? html`
|
||||
<div class="account-card-list">
|
||||
${telegramAccounts.map((account) => renderAccountCard(account))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Running</span>
|
||||
<span>${telegram?.running ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Mode</span>
|
||||
<span>${telegram?.mode ?? "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${telegram?.lastStartAt ? formatRelativeTimestamp(telegram.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${telegram?.lastProbeAt ? formatRelativeTimestamp(telegram.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
<div class="account-card-list">
|
||||
${telegramAccounts.map((account) => renderAccountCard(account))}
|
||||
</div>
|
||||
|
||||
${
|
||||
telegram?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${telegram.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
telegram?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${telegram.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
telegram?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${telegram.probe.ok ? "ok" : "failed"} ·
|
||||
${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
telegram?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${telegram.probe.ok ? "ok" : "failed"} ·
|
||||
${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "telegram", props })}
|
||||
${renderChannelConfigSection({ channelId: "telegram", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
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`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${telegram.probe.ok ? "ok" : "failed"} ·
|
||||
${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "telegram", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<string, ChannelAccountSnapshot[]>,
|
||||
) {
|
||||
const label = resolveChannelLabel(props.snapshot, key);
|
||||
const status = props.snapshot?.channels?.[key] as Record<string, unknown> | 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(
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
|
||||
<span>${formatNullableBoolean(displayState.configured)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Running</span>
|
||||
<span>${running == null ? "n/a" : running ? "Yes" : "No"}</span>
|
||||
<span>${formatNullableBoolean(displayState.running)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Connected</span>
|
||||
<span>${connected == null ? "n/a" : connected ? "Yes" : "No"}</span>
|
||||
<span>${formatNullableBoolean(displayState.connected)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -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`
|
||||
<div class="card">
|
||||
<div class="card-title">WhatsApp</div>
|
||||
<div class="card-sub">Link WhatsApp Web and monitor connection health.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
<span>${configured == null ? "n/a" : configured ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Linked</span>
|
||||
<span>${whatsapp?.linked ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Running</span>
|
||||
<span>${whatsapp?.running ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Connected</span>
|
||||
<span>${whatsapp?.connected ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last connect</span>
|
||||
<span>
|
||||
${whatsapp?.lastConnectedAt ? formatRelativeTimestamp(whatsapp.lastConnectedAt) : "n/a"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last message</span>
|
||||
<span>
|
||||
${whatsapp?.lastMessageAt ? formatRelativeTimestamp(whatsapp.lastMessageAt) : "n/a"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Auth age</span>
|
||||
<span>
|
||||
${whatsapp?.authAgeMs != null ? formatDurationHuman(whatsapp.authAgeMs) : "n/a"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
whatsapp?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${whatsapp.lastError}
|
||||
</div>`
|
||||
: 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`<div class="callout" style="margin-top: 12px;">
|
||||
${props.whatsappMessage}
|
||||
</div>`
|
||||
${props.whatsappMessage}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
props.whatsappQrDataUrl
|
||||
? html`<div class="qr-wrap">
|
||||
<img src=${props.whatsappQrDataUrl} alt="WhatsApp QR" />
|
||||
</div>`
|
||||
<img src=${props.whatsappQrDataUrl} alt="WhatsApp QR" />
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="row" style="margin-top: 14px; flex-wrap: wrap;">
|
||||
<button
|
||||
class="btn primary"
|
||||
?disabled=${props.whatsappBusy}
|
||||
@click=${() => props.onWhatsAppStart(false)}
|
||||
>
|
||||
${props.whatsappBusy ? "Working…" : "Show QR"}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${props.whatsappBusy}
|
||||
@click=${() => props.onWhatsAppStart(true)}
|
||||
>
|
||||
Relink
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${props.whatsappBusy}
|
||||
@click=${() => props.onWhatsAppWait()}
|
||||
>
|
||||
Wait for scan
|
||||
</button>
|
||||
<button
|
||||
class="btn danger"
|
||||
?disabled=${props.whatsappBusy}
|
||||
@click=${() => props.onWhatsAppLogout()}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${renderChannelConfigSection({ channelId: "whatsapp", props })}
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
configSection: renderChannelConfigSection({ channelId: "whatsapp", props }),
|
||||
footer: html`<div class="row" style="margin-top: 14px; flex-wrap: wrap;">
|
||||
<button
|
||||
class="btn primary"
|
||||
?disabled=${props.whatsappBusy}
|
||||
@click=${() => props.onWhatsAppStart(false)}
|
||||
>
|
||||
${props.whatsappBusy ? "Working…" : "Show QR"}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${props.whatsappBusy}
|
||||
@click=${() => props.onWhatsAppStart(true)}
|
||||
>
|
||||
Relink
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${props.whatsappBusy}
|
||||
@click=${() => props.onWhatsAppWait()}
|
||||
>
|
||||
Wait for scan
|
||||
</button>
|
||||
<button
|
||||
class="btn danger"
|
||||
?disabled=${props.whatsappBusy}
|
||||
@click=${() => props.onWhatsAppLogout()}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ export default defineConfig({
|
||||
"test/**/*.test.ts",
|
||||
"ui/src/ui/app-chat.test.ts",
|
||||
"ui/src/ui/views/agents-utils.test.ts",
|
||||
"ui/src/ui/views/channels.test.ts",
|
||||
"ui/src/ui/views/chat.test.ts",
|
||||
"ui/src/ui/views/nodes.devices.test.ts",
|
||||
"ui/src/ui/views/usage-render-details.test.ts",
|
||||
|
||||
@@ -5,6 +5,7 @@ export const unitTestIncludePatterns = [
|
||||
"test/**/*.test.ts",
|
||||
"ui/src/ui/app-chat.test.ts",
|
||||
"ui/src/ui/views/agents-utils.test.ts",
|
||||
"ui/src/ui/views/channels.test.ts",
|
||||
"ui/src/ui/views/chat.test.ts",
|
||||
"ui/src/ui/views/usage-render-details.test.ts",
|
||||
"ui/src/ui/controllers/agents.test.ts",
|
||||
|
||||
Reference in New Issue
Block a user