1008 lines
35 KiB
TypeScript
1008 lines
35 KiB
TypeScript
import { html, nothing } from "lit";
|
|
|
|
import { formatAgo } from "../format";
|
|
import type { ProvidersStatusSnapshot } from "../types";
|
|
import type { DiscordForm, IMessageForm, SignalForm, TelegramForm } from "../ui-types";
|
|
|
|
export type ConnectionsProps = {
|
|
connected: boolean;
|
|
loading: boolean;
|
|
snapshot: ProvidersStatusSnapshot | null;
|
|
lastError: string | null;
|
|
lastSuccessAt: number | null;
|
|
whatsappMessage: string | null;
|
|
whatsappQrDataUrl: string | null;
|
|
whatsappConnected: boolean | null;
|
|
whatsappBusy: boolean;
|
|
telegramForm: TelegramForm;
|
|
telegramTokenLocked: boolean;
|
|
telegramSaving: boolean;
|
|
telegramStatus: string | null;
|
|
discordForm: DiscordForm;
|
|
discordTokenLocked: boolean;
|
|
discordSaving: boolean;
|
|
discordStatus: string | null;
|
|
signalForm: SignalForm;
|
|
signalSaving: boolean;
|
|
signalStatus: string | null;
|
|
imessageForm: IMessageForm;
|
|
imessageSaving: boolean;
|
|
imessageStatus: string | null;
|
|
onRefresh: (probe: boolean) => void;
|
|
onWhatsAppStart: (force: boolean) => void;
|
|
onWhatsAppWait: () => void;
|
|
onWhatsAppLogout: () => void;
|
|
onTelegramChange: (patch: Partial<TelegramForm>) => void;
|
|
onTelegramSave: () => void;
|
|
onDiscordChange: (patch: Partial<DiscordForm>) => void;
|
|
onDiscordSave: () => void;
|
|
onSignalChange: (patch: Partial<SignalForm>) => void;
|
|
onSignalSave: () => void;
|
|
onIMessageChange: (patch: Partial<IMessageForm>) => void;
|
|
onIMessageSave: () => void;
|
|
};
|
|
|
|
export function renderConnections(props: ConnectionsProps) {
|
|
const whatsapp = props.snapshot?.whatsapp;
|
|
const telegram = props.snapshot?.telegram;
|
|
const discord = props.snapshot?.discord ?? null;
|
|
const signal = props.snapshot?.signal ?? null;
|
|
const imessage = props.snapshot?.imessage ?? null;
|
|
const providerOrder: ProviderKey[] = [
|
|
"whatsapp",
|
|
"telegram",
|
|
"discord",
|
|
"signal",
|
|
"imessage",
|
|
];
|
|
const orderedProviders = providerOrder
|
|
.map((key, index) => ({
|
|
key,
|
|
enabled: providerEnabled(key, props),
|
|
order: index,
|
|
}))
|
|
.sort((a, b) => {
|
|
if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
|
|
return a.order - b.order;
|
|
});
|
|
|
|
return html`
|
|
<section class="grid grid-cols-2">
|
|
${orderedProviders.map((provider) =>
|
|
renderProvider(provider.key, props, { whatsapp, telegram, discord, signal, imessage }),
|
|
)}
|
|
</section>
|
|
|
|
<section class="card" style="margin-top: 18px;">
|
|
<div class="row" style="justify-content: space-between;">
|
|
<div>
|
|
<div class="card-title">Connection health</div>
|
|
<div class="card-sub">Provider status snapshots from the gateway.</div>
|
|
</div>
|
|
<div class="muted">${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : "n/a"}</div>
|
|
</div>
|
|
${props.lastError
|
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
|
${props.lastError}
|
|
</div>`
|
|
: nothing}
|
|
<pre class="code-block" style="margin-top: 12px;">
|
|
${props.snapshot ? JSON.stringify(props.snapshot, null, 2) : "No snapshot yet."}
|
|
</pre>
|
|
</section>
|
|
`;
|
|
}
|
|
|
|
function formatDuration(ms?: number | null) {
|
|
if (!ms && ms !== 0) return "n/a";
|
|
const sec = Math.round(ms / 1000);
|
|
if (sec < 60) return `${sec}s`;
|
|
const min = Math.round(sec / 60);
|
|
if (min < 60) return `${min}m`;
|
|
const hr = Math.round(min / 60);
|
|
return `${hr}h`;
|
|
}
|
|
|
|
type ProviderKey = "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
|
|
|
|
function providerEnabled(key: ProviderKey, props: ConnectionsProps) {
|
|
const snapshot = props.snapshot;
|
|
if (!snapshot) return false;
|
|
switch (key) {
|
|
case "whatsapp":
|
|
return (
|
|
snapshot.whatsapp.configured ||
|
|
snapshot.whatsapp.linked ||
|
|
snapshot.whatsapp.running
|
|
);
|
|
case "telegram":
|
|
return snapshot.telegram.configured || snapshot.telegram.running;
|
|
case "discord":
|
|
return Boolean(snapshot.discord?.configured || snapshot.discord?.running);
|
|
case "signal":
|
|
return Boolean(snapshot.signal?.configured || snapshot.signal?.running);
|
|
case "imessage":
|
|
return Boolean(snapshot.imessage?.configured || snapshot.imessage?.running);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function renderProvider(
|
|
key: ProviderKey,
|
|
props: ConnectionsProps,
|
|
data: {
|
|
whatsapp?: ProvidersStatusSnapshot["whatsapp"];
|
|
telegram?: ProvidersStatusSnapshot["telegram"];
|
|
discord?: ProvidersStatusSnapshot["discord"] | null;
|
|
signal?: ProvidersStatusSnapshot["signal"] | null;
|
|
imessage?: ProvidersStatusSnapshot["imessage"] | null;
|
|
},
|
|
) {
|
|
switch (key) {
|
|
case "whatsapp": {
|
|
const whatsapp = data.whatsapp;
|
|
return html`
|
|
<div class="card">
|
|
<div class="card-title">WhatsApp</div>
|
|
<div class="card-sub">Link WhatsApp Web and monitor connection health.</div>
|
|
|
|
<div class="status-list" style="margin-top: 16px;">
|
|
<div>
|
|
<span class="label">Configured</span>
|
|
<span>${whatsapp?.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
|
|
? formatAgo(whatsapp.lastConnectedAt)
|
|
: "n/a"}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Last message</span>
|
|
<span>
|
|
${whatsapp?.lastMessageAt ? formatAgo(whatsapp.lastMessageAt) : "n/a"}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Auth age</span>
|
|
<span>
|
|
${whatsapp?.authAgeMs != null ? formatDuration(whatsapp.authAgeMs) : "n/a"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
${whatsapp?.lastError
|
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
|
${whatsapp.lastError}
|
|
</div>`
|
|
: nothing}
|
|
|
|
${props.whatsappMessage
|
|
? html`<div class="callout" style="margin-top: 12px;">
|
|
${props.whatsappMessage}
|
|
</div>`
|
|
: nothing}
|
|
|
|
${props.whatsappQrDataUrl
|
|
? html`<div class="qr-wrap">
|
|
<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>
|
|
</div>
|
|
`;
|
|
}
|
|
case "telegram": {
|
|
const telegram = data.telegram;
|
|
return html`
|
|
<div class="card">
|
|
<div class="card-title">Telegram</div>
|
|
<div class="card-sub">Bot token and delivery options.</div>
|
|
|
|
<div class="status-list" style="margin-top: 16px;">
|
|
<div>
|
|
<span class="label">Configured</span>
|
|
<span>${telegram?.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 ? formatAgo(telegram.lastStartAt) : "n/a"}</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Last probe</span>
|
|
<span>${telegram?.lastProbeAt ? formatAgo(telegram.lastProbeAt) : "n/a"}</span>
|
|
</div>
|
|
</div>
|
|
|
|
${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}
|
|
|
|
<div class="form-grid" style="margin-top: 16px;">
|
|
<label class="field">
|
|
<span>Bot token</span>
|
|
<input
|
|
type="password"
|
|
.value=${props.telegramForm.token}
|
|
?disabled=${props.telegramTokenLocked}
|
|
@input=${(e: Event) =>
|
|
props.onTelegramChange({
|
|
token: (e.target as HTMLInputElement).value,
|
|
})}
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Require mention in groups</span>
|
|
<select
|
|
.value=${props.telegramForm.requireMention ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onTelegramChange({
|
|
requireMention: (e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Yes</option>
|
|
<option value="no">No</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Allow from</span>
|
|
<input
|
|
.value=${props.telegramForm.allowFrom}
|
|
@input=${(e: Event) =>
|
|
props.onTelegramChange({
|
|
allowFrom: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="123456789, @team"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Proxy</span>
|
|
<input
|
|
.value=${props.telegramForm.proxy}
|
|
@input=${(e: Event) =>
|
|
props.onTelegramChange({
|
|
proxy: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="socks5://localhost:9050"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Webhook URL</span>
|
|
<input
|
|
.value=${props.telegramForm.webhookUrl}
|
|
@input=${(e: Event) =>
|
|
props.onTelegramChange({
|
|
webhookUrl: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="https://example.com/telegram-webhook"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Webhook secret</span>
|
|
<input
|
|
.value=${props.telegramForm.webhookSecret}
|
|
@input=${(e: Event) =>
|
|
props.onTelegramChange({
|
|
webhookSecret: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="secret"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Webhook path</span>
|
|
<input
|
|
.value=${props.telegramForm.webhookPath}
|
|
@input=${(e: Event) =>
|
|
props.onTelegramChange({
|
|
webhookPath: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="/telegram-webhook"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
${props.telegramTokenLocked
|
|
? html`<div class="callout" style="margin-top: 12px;">
|
|
TELEGRAM_BOT_TOKEN is set in the environment. Config edits will not override it.
|
|
</div>`
|
|
: nothing}
|
|
|
|
${props.telegramStatus
|
|
? html`<div class="callout" style="margin-top: 12px;">
|
|
${props.telegramStatus}
|
|
</div>`
|
|
: nothing}
|
|
|
|
<div class="row" style="margin-top: 14px;">
|
|
<button
|
|
class="btn primary"
|
|
?disabled=${props.telegramSaving}
|
|
@click=${() => props.onTelegramSave()}
|
|
>
|
|
${props.telegramSaving ? "Saving…" : "Save"}
|
|
</button>
|
|
<button class="btn" @click=${() => props.onRefresh(true)}>
|
|
Probe
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
case "discord": {
|
|
const discord = data.discord;
|
|
const botName = discord?.probe?.bot?.username;
|
|
return html`
|
|
<div class="card">
|
|
<div class="card-title">Discord</div>
|
|
<div class="card-sub">Bot connection and probe status.</div>
|
|
|
|
<div class="status-list" style="margin-top: 16px;">
|
|
<div>
|
|
<span class="label">Configured</span>
|
|
<span>${discord?.configured ? "Yes" : "No"}</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Running</span>
|
|
<span>${discord?.running ? "Yes" : "No"}</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Bot</span>
|
|
<span>${botName ? `@${botName}` : "n/a"}</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Last start</span>
|
|
<span>${discord?.lastStartAt ? formatAgo(discord.lastStartAt) : "n/a"}</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Last probe</span>
|
|
<span>${discord?.lastProbeAt ? formatAgo(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}
|
|
|
|
<div class="form-grid" style="margin-top: 16px;">
|
|
<label class="field">
|
|
<span>Enabled</span>
|
|
<select
|
|
.value=${props.discordForm.enabled ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
enabled: (e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Yes</option>
|
|
<option value="no">No</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Bot token</span>
|
|
<input
|
|
type="password"
|
|
.value=${props.discordForm.token}
|
|
?disabled=${props.discordTokenLocked}
|
|
@input=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
token: (e.target as HTMLInputElement).value,
|
|
})}
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Allow DMs from</span>
|
|
<input
|
|
.value=${props.discordForm.allowFrom}
|
|
@input=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
allowFrom: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="123456789, username#1234"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Group DMs</span>
|
|
<select
|
|
.value=${props.discordForm.groupEnabled ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
groupEnabled: (e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Enabled</option>
|
|
<option value="no">Disabled</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Group channels</span>
|
|
<input
|
|
.value=${props.discordForm.groupChannels}
|
|
@input=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
groupChannels: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="channelId1, channelId2"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Media max MB</span>
|
|
<input
|
|
.value=${props.discordForm.mediaMaxMb}
|
|
@input=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
mediaMaxMb: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="8"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>History limit</span>
|
|
<input
|
|
.value=${props.discordForm.historyLimit}
|
|
@input=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
historyLimit: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="20"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Slash command</span>
|
|
<select
|
|
.value=${props.discordForm.slashEnabled ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
slashEnabled: (e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Enabled</option>
|
|
<option value="no">Disabled</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Slash name</span>
|
|
<input
|
|
.value=${props.discordForm.slashName}
|
|
@input=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
slashName: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="clawd"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Slash session prefix</span>
|
|
<input
|
|
.value=${props.discordForm.slashSessionPrefix}
|
|
@input=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
slashSessionPrefix: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="discord:slash"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Slash ephemeral</span>
|
|
<select
|
|
.value=${props.discordForm.slashEphemeral ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onDiscordChange({
|
|
slashEphemeral: (e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Yes</option>
|
|
<option value="no">No</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
${props.discordTokenLocked
|
|
? html`<div class="callout" style="margin-top: 12px;">
|
|
DISCORD_BOT_TOKEN is set in the environment. Config edits will not override it.
|
|
</div>`
|
|
: nothing}
|
|
|
|
${props.discordStatus
|
|
? html`<div class="callout" style="margin-top: 12px;">
|
|
${props.discordStatus}
|
|
</div>`
|
|
: nothing}
|
|
|
|
<div class="row" style="margin-top: 14px;">
|
|
<button
|
|
class="btn primary"
|
|
?disabled=${props.discordSaving}
|
|
@click=${() => props.onDiscordSave()}
|
|
>
|
|
${props.discordSaving ? "Saving…" : "Save"}
|
|
</button>
|
|
<button class="btn" @click=${() => props.onRefresh(true)}>
|
|
Probe
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
case "signal": {
|
|
const signal = data.signal;
|
|
return html`
|
|
<div class="card">
|
|
<div class="card-title">Signal</div>
|
|
<div class="card-sub">REST daemon status and probe details.</div>
|
|
|
|
<div class="status-list" style="margin-top: 16px;">
|
|
<div>
|
|
<span class="label">Configured</span>
|
|
<span>${signal?.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 ? formatAgo(signal.lastStartAt) : "n/a"}</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Last probe</span>
|
|
<span>${signal?.lastProbeAt ? formatAgo(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}
|
|
|
|
<div class="form-grid" style="margin-top: 16px;">
|
|
<label class="field">
|
|
<span>Enabled</span>
|
|
<select
|
|
.value=${props.signalForm.enabled ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onSignalChange({
|
|
enabled: (e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Yes</option>
|
|
<option value="no">No</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Account</span>
|
|
<input
|
|
.value=${props.signalForm.account}
|
|
@input=${(e: Event) =>
|
|
props.onSignalChange({
|
|
account: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="+15551234567"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>HTTP URL</span>
|
|
<input
|
|
.value=${props.signalForm.httpUrl}
|
|
@input=${(e: Event) =>
|
|
props.onSignalChange({
|
|
httpUrl: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="http://127.0.0.1:8080"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>HTTP host</span>
|
|
<input
|
|
.value=${props.signalForm.httpHost}
|
|
@input=${(e: Event) =>
|
|
props.onSignalChange({
|
|
httpHost: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="127.0.0.1"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>HTTP port</span>
|
|
<input
|
|
.value=${props.signalForm.httpPort}
|
|
@input=${(e: Event) =>
|
|
props.onSignalChange({
|
|
httpPort: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="8080"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>CLI path</span>
|
|
<input
|
|
.value=${props.signalForm.cliPath}
|
|
@input=${(e: Event) =>
|
|
props.onSignalChange({
|
|
cliPath: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="signal-cli"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Auto start</span>
|
|
<select
|
|
.value=${props.signalForm.autoStart ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onSignalChange({
|
|
autoStart: (e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Yes</option>
|
|
<option value="no">No</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Receive mode</span>
|
|
<select
|
|
.value=${props.signalForm.receiveMode}
|
|
@change=${(e: Event) =>
|
|
props.onSignalChange({
|
|
receiveMode: (e.target as HTMLSelectElement).value as
|
|
| "on-start"
|
|
| "manual"
|
|
| "",
|
|
})}
|
|
>
|
|
<option value="">Default</option>
|
|
<option value="on-start">on-start</option>
|
|
<option value="manual">manual</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Ignore attachments</span>
|
|
<select
|
|
.value=${props.signalForm.ignoreAttachments ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onSignalChange({
|
|
ignoreAttachments:
|
|
(e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Yes</option>
|
|
<option value="no">No</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Ignore stories</span>
|
|
<select
|
|
.value=${props.signalForm.ignoreStories ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onSignalChange({
|
|
ignoreStories: (e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Yes</option>
|
|
<option value="no">No</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Send read receipts</span>
|
|
<select
|
|
.value=${props.signalForm.sendReadReceipts ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onSignalChange({
|
|
sendReadReceipts:
|
|
(e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Yes</option>
|
|
<option value="no">No</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Allow from</span>
|
|
<input
|
|
.value=${props.signalForm.allowFrom}
|
|
@input=${(e: Event) =>
|
|
props.onSignalChange({
|
|
allowFrom: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="12345, +1555"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Media max MB</span>
|
|
<input
|
|
.value=${props.signalForm.mediaMaxMb}
|
|
@input=${(e: Event) =>
|
|
props.onSignalChange({
|
|
mediaMaxMb: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="8"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
${props.signalStatus
|
|
? html`<div class="callout" style="margin-top: 12px;">
|
|
${props.signalStatus}
|
|
</div>`
|
|
: nothing}
|
|
|
|
<div class="row" style="margin-top: 14px;">
|
|
<button
|
|
class="btn primary"
|
|
?disabled=${props.signalSaving}
|
|
@click=${() => props.onSignalSave()}
|
|
>
|
|
${props.signalSaving ? "Saving…" : "Save"}
|
|
</button>
|
|
<button class="btn" @click=${() => props.onRefresh(true)}>
|
|
Probe
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
case "imessage": {
|
|
const imessage = data.imessage;
|
|
return html`
|
|
<div class="card">
|
|
<div class="card-title">iMessage</div>
|
|
<div class="card-sub">imsg CLI and database availability.</div>
|
|
|
|
<div class="status-list" style="margin-top: 16px;">
|
|
<div>
|
|
<span class="label">Configured</span>
|
|
<span>${imessage?.configured ? "Yes" : "No"}</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Running</span>
|
|
<span>${imessage?.running ? "Yes" : "No"}</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">CLI</span>
|
|
<span>${imessage?.cliPath ?? "n/a"}</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">DB</span>
|
|
<span>${imessage?.dbPath ?? "n/a"}</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Last start</span>
|
|
<span>
|
|
${imessage?.lastStartAt ? formatAgo(imessage.lastStartAt) : "n/a"}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span class="label">Last probe</span>
|
|
<span>
|
|
${imessage?.lastProbeAt ? formatAgo(imessage.lastProbeAt) : "n/a"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
${imessage?.lastError
|
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
|
${imessage.lastError}
|
|
</div>`
|
|
: nothing}
|
|
|
|
${imessage?.probe && !imessage.probe.ok
|
|
? html`<div class="callout" style="margin-top: 12px;">
|
|
Probe failed · ${imessage.probe.error ?? "unknown error"}
|
|
</div>`
|
|
: nothing}
|
|
|
|
<div class="form-grid" style="margin-top: 16px;">
|
|
<label class="field">
|
|
<span>Enabled</span>
|
|
<select
|
|
.value=${props.imessageForm.enabled ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onIMessageChange({
|
|
enabled: (e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Yes</option>
|
|
<option value="no">No</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>CLI path</span>
|
|
<input
|
|
.value=${props.imessageForm.cliPath}
|
|
@input=${(e: Event) =>
|
|
props.onIMessageChange({
|
|
cliPath: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="imsg"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>DB path</span>
|
|
<input
|
|
.value=${props.imessageForm.dbPath}
|
|
@input=${(e: Event) =>
|
|
props.onIMessageChange({
|
|
dbPath: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="~/Library/Messages/chat.db"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Service</span>
|
|
<select
|
|
.value=${props.imessageForm.service}
|
|
@change=${(e: Event) =>
|
|
props.onIMessageChange({
|
|
service: (e.target as HTMLSelectElement).value as
|
|
| "auto"
|
|
| "imessage"
|
|
| "sms",
|
|
})}
|
|
>
|
|
<option value="auto">Auto</option>
|
|
<option value="imessage">iMessage</option>
|
|
<option value="sms">SMS</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Region</span>
|
|
<input
|
|
.value=${props.imessageForm.region}
|
|
@input=${(e: Event) =>
|
|
props.onIMessageChange({
|
|
region: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="US"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Allow from</span>
|
|
<input
|
|
.value=${props.imessageForm.allowFrom}
|
|
@input=${(e: Event) =>
|
|
props.onIMessageChange({
|
|
allowFrom: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="chat_id:101, +1555"
|
|
/>
|
|
</label>
|
|
<label class="field">
|
|
<span>Include attachments</span>
|
|
<select
|
|
.value=${props.imessageForm.includeAttachments ? "yes" : "no"}
|
|
@change=${(e: Event) =>
|
|
props.onIMessageChange({
|
|
includeAttachments:
|
|
(e.target as HTMLSelectElement).value === "yes",
|
|
})}
|
|
>
|
|
<option value="yes">Yes</option>
|
|
<option value="no">No</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Media max MB</span>
|
|
<input
|
|
.value=${props.imessageForm.mediaMaxMb}
|
|
@input=${(e: Event) =>
|
|
props.onIMessageChange({
|
|
mediaMaxMb: (e.target as HTMLInputElement).value,
|
|
})}
|
|
placeholder="16"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
${props.imessageStatus
|
|
? html`<div class="callout" style="margin-top: 12px;">
|
|
${props.imessageStatus}
|
|
</div>`
|
|
: nothing}
|
|
|
|
<div class="row" style="margin-top: 14px;">
|
|
<button
|
|
class="btn primary"
|
|
?disabled=${props.imessageSaving}
|
|
@click=${() => props.onIMessageSave()}
|
|
>
|
|
${props.imessageSaving ? "Saving…" : "Save"}
|
|
</button>
|
|
<button class="btn" @click=${() => props.onRefresh(true)}>
|
|
Probe
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
default:
|
|
return nothing;
|
|
}
|
|
}
|