177 lines
21 KiB
HTML
177 lines
21 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="color-scheme" content="light" />
|
||
<meta name="referrer" content="no-referrer" />
|
||
<title data-i18n="app.title">Service Configuration · MessageWeave</title>
|
||
<link rel="stylesheet" href="/assets/styles.css" />
|
||
<script src="/assets/config.js" defer></script>
|
||
</head>
|
||
<body>
|
||
<div class="ambient ambient-one" aria-hidden="true"></div>
|
||
<div class="ambient ambient-two" aria-hidden="true"></div>
|
||
|
||
<header class="topbar">
|
||
<a class="brand" href="/" data-i18n-aria="brand.aria" aria-label="MessageWeave configuration home">
|
||
<span class="brand-mark" aria-hidden="true">S</span>
|
||
<span class="brand-copy"><strong>MessageWeave</strong><span data-i18n="brand.tagline">Service configuration</span></span>
|
||
</a>
|
||
<span class="topbar-note"><span class="secure-dot"></span><span data-i18n="topbar.note">Secure configuration channel</span></span>
|
||
</header>
|
||
|
||
<main class="layout">
|
||
<section id="intro" class="intro" aria-labelledby="page-title" hidden>
|
||
<h1 id="page-title" data-i18n="intro.title">Manage your service connections</h1>
|
||
<p class="intro-copy" data-i18n="intro.copy">Configure JMAP, Telegram, LLM, access allowlist and runtime parameters. Business configuration is stored in external Redis and hot-reloaded immediately after a successful save.</p>
|
||
</section>
|
||
|
||
<div id="notice" class="notice notice-info" role="status" aria-live="polite" hidden data-i18n="notice.bootstrap">
|
||
Create a short-lived admin session using the CONFIG_ENCRYPTION_KEY value.
|
||
</div>
|
||
|
||
<section id="setup-card" class="panel auth-panel setup-panel" aria-labelledby="setup-title" aria-live="polite">
|
||
<div class="panel-heading">
|
||
<div class="icon-tile icon-violet" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24"><path d="M12 3 4 7v5c0 4.4 3.1 7.6 8 9 4.9-1.4 8-4.6 8-9V7l-8-4Z"/><path d="M12 8v5M12 16h.01"/></svg>
|
||
</div>
|
||
<div><p class="eyebrow" data-i18n="setup.eyebrow">Setup guide</p><h2 id="setup-title" data-i18n="setup.title">Service startup configuration</h2></div>
|
||
</div>
|
||
<p id="setup-message" class="panel-copy" data-i18n="setup.checking">Checking service startup configuration…</p>
|
||
<p id="setup-missing" class="setup-missing" hidden></p>
|
||
<p class="setup-privacy" data-i18n="setup.privacy">Startup secrets must be configured in the deployment environment. Secrets are never saved or echoed in the page.</p>
|
||
<button id="status-retry-button" class="button button-quiet" type="button" data-i18n="setup.retry">Re-check</button>
|
||
</section>
|
||
|
||
<section id="auth-card" class="panel auth-panel" aria-labelledby="auth-title" hidden>
|
||
<div class="panel-heading">
|
||
<div class="icon-tile icon-violet" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24"><path d="M7.5 10V7.5a4.5 4.5 0 0 1 9 0V10M6 10h12v10H6z"/><path d="M12 14v2"/></svg>
|
||
</div>
|
||
<div><p class="eyebrow" data-i18n="auth.eyebrow">Authorization required</p><h2 id="auth-title" data-i18n="auth.title">Create admin session</h2></div>
|
||
</div>
|
||
<p class="panel-copy"><span data-i18n="auth.bodyPre">Enter </span><code>CONFIG_ENCRYPTION_KEY</code><span data-i18n="auth.bodyPost">. The session lasts 30 minutes and lives only in this page's memory.</span></p>
|
||
<form id="auth-form" class="auth-form" autocomplete="off">
|
||
<label for="bootstrap-input" data-i18n="auth.label">Business encryption key</label>
|
||
<div class="token-row">
|
||
<input id="bootstrap-input" name="admin-credential" type="password" autocomplete="new-password" spellcheck="false" required data-i18n-placeholder="auth.placeholder" placeholder="Paste the value" aria-describedby="bootstrap-hint" />
|
||
<button id="connect-button" class="button button-primary" type="submit" data-i18n="auth.submit">Create session</button>
|
||
</div>
|
||
<p id="bootstrap-hint" class="field-hint" data-i18n="auth.hint">You must re-authorize after closing or refreshing the page.</p>
|
||
</form>
|
||
</section>
|
||
|
||
<section id="config-panel" class="config-panel" data-i18n-aria="config.aria" aria-label="Business configuration and runtime parameters" hidden>
|
||
<div class="panel config-toolbar">
|
||
<div class="session-state"><span class="status-pulse" aria-hidden="true"></span><div><strong data-i18n="session.active">Admin session active</strong><span id="session-expiry" data-i18n="session.expiryDefault">Session kept only in current page memory</span></div></div>
|
||
<div class="toolbar-actions">
|
||
<label class="toggle-card" for="service-enabled"><input id="service-enabled" type="checkbox" /><span><strong data-i18n="service.enable">Enable business processing</strong><small id="service-enabled-state" data-i18n="service.loading">Loading…</small></span></label>
|
||
<button id="reload-button" class="button button-quiet" type="button" data-i18n="reload.button">Reload runtime parameters</button>
|
||
<button id="logout-button" class="button button-quiet" type="button" data-i18n="logout.button">Sign out and clear session</button>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="panel config-section" aria-labelledby="business-title">
|
||
<div class="section-heading">
|
||
<div><p class="eyebrow" data-i18n="business.eyebrow">Full replacement</p><h2 id="business-title" data-i18n="business.title">Business connections and auth</h2></div>
|
||
<span class="persist-label"><span class="persist-dot"></span><span data-i18n="business.persist">Encrypted in Redis</span></span>
|
||
</div>
|
||
<div class="callout callout-warning">
|
||
<strong data-i18n="callout.title">Business configuration is encrypted in Redis.</strong>
|
||
<span data-i18n="callout.bodyPre"> Secret fields (app passwords, tokens) are not echoed and must be re-entered on each full save; non-secret fields are read back from the server. The first save must include all required secrets. Optional LLM fields are sent as </span><code>null</code><span data-i18n="callout.bodyPost"> when left blank. The mailbox primary account is always used.</span>
|
||
</div>
|
||
|
||
<form id="business-form" novalidate>
|
||
<fieldset id="business-fields" disabled>
|
||
<legend class="visually-hidden" data-i18n="legend.business">Full business configuration</legend>
|
||
|
||
<section class="form-section" aria-labelledby="jmap-title">
|
||
<div class="form-section-heading"><span class="section-number">01</span><div><h3 id="jmap-title" data-i18n="jmap.title">JMAP mailbox</h3><p data-i18n="jmap.desc">The server must use HTTPS; the URL must not contain credentials, query parameters or fragments.</p></div></div>
|
||
<div class="field-grid">
|
||
<div class="field field-wide"><label for="jmap-session-url">JMAP Session URL <span class="required-mark" data-i18n="common.required">Required</span></label><input id="jmap-session-url" name="jmap_session_url" type="url" inputmode="url" autocomplete="url" required placeholder="https://mail.example.com/.well-known/jmap" /><small data-i18n="jmap.urlHint">HTTPS address without username, password, query parameters or #fragment.</small></div>
|
||
<div class="field"><label for="jmap-username"><span data-i18n="jmap.userLabel">Email username</span> <span class="required-mark" data-i18n="common.required">Required</span></label><input id="jmap-username" name="jmap_username" type="text" autocomplete="username" required /></div>
|
||
<div class="field"><label for="jmap-password"><span data-i18n="jmap.passLabel">App-specific password</span> <span class="required-mark" data-i18n="common.required">Required</span></label><input id="jmap-password" name="jmap_password" type="password" autocomplete="new-password" spellcheck="false" required data-i18n-placeholder="secret.reenter" placeholder="Re-enter on each full save" /><small id="secret-marker-jmap_password" class="secret-marker" role="status" aria-live="polite"></small></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="form-section" aria-labelledby="telegram-title">
|
||
<div class="form-section-heading"><span class="section-number">02</span><div><h3 id="telegram-title" data-i18n="telegram.title">Telegram and access auth</h3><p data-i18n="telegram.desc">Only chat IDs in the allowlist may trigger bot operations.</p></div></div>
|
||
<div class="field-grid">
|
||
<div class="field"><label for="telegram-bot-token">Telegram Bot Token <span class="required-mark" data-i18n="common.required">Required</span></label><input id="telegram-bot-token" name="bot_token" type="password" autocomplete="new-password" spellcheck="false" required data-i18n-placeholder="secret.reenter" placeholder="Re-enter on each full save" /><small id="secret-marker-bot_token" class="secret-marker" role="status" aria-live="polite"></small></div>
|
||
<div class="field"><label for="telegram-chat-id"><span data-i18n="telegram.chatLabel">Target Chat ID</span> <span class="required-mark" data-i18n="common.required">Required</span></label><input id="telegram-chat-id" name="telegram_chat_id" type="text" inputmode="numeric" autocomplete="off" required data-i18n-placeholder="telegram.chatPh" placeholder="e.g. -1001234567890" /><small data-i18n="telegram.chatHint">Accepts a full signed 64-bit integer.</small></div>
|
||
<div class="field field-wide"><label for="chat-allowlist">Chat allowlist <span class="required-mark" data-i18n="common.atLeastOne">At least one</span></label><textarea id="chat-allowlist" name="chat_allowlist" rows="3" required spellcheck="false" data-i18n-placeholder="telegram.allowPh" placeholder="One chat ID per line or comma-separated"></textarea><small data-i18n="telegram.allowHint">Integer list; format and duplicates are checked before saving.</small></div>
|
||
<div class="field"><label for="telegram-webhook-secret">Telegram Webhook Secret <span class="required-mark" data-i18n="common.required">Required</span></label><input id="telegram-webhook-secret" name="telegram_webhook_secret" type="password" autocomplete="new-password" spellcheck="false" required data-i18n-placeholder="secret.reenter" placeholder="Re-enter on each full save" /><small id="secret-marker-telegram_webhook_secret" class="secret-marker" role="status" aria-live="polite"></small></div>
|
||
<div class="field"><label for="reconcile-token">Reconcile Bearer Token <span class="required-mark" data-i18n="common.required">Required</span></label><input id="reconcile-token" name="reconcile_token" type="password" autocomplete="new-password" spellcheck="false" required data-i18n-placeholder="secret.reenter" placeholder="Re-enter on each full save" /><small id="secret-marker-reconcile_token" class="secret-marker" role="status" aria-live="polite"></small></div>
|
||
<div class="field field-wide"><label for="worker-token">Worker Token <span class="required-mark" data-i18n="common.required">Required</span></label><input id="worker-token" name="worker_token" type="password" autocomplete="new-password" spellcheck="false" required data-i18n-placeholder="telegram.workerPh" placeholder="Used by /worker and compatible admin APIs; re-enter on each full save" /><small id="secret-marker-worker_token" class="secret-marker" role="status" aria-live="polite"></small><small data-i18n="telegram.workerHint">This value differs from the current admin session. The page never reads or echoes an existing token.</small></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="form-section" aria-labelledby="llm-title">
|
||
<div class="form-section-heading"><span class="section-number">03</span><div><h3 id="llm-title" data-i18n="llm.title">LLM service</h3><p data-i18n="llm.desc">When enabled, an HTTPS Base URL, model name and API key are required.</p></div></div>
|
||
<div class="toggle-grid">
|
||
<label class="toggle-card" for="llm-enabled"><input id="llm-enabled" name="llm_enabled" type="checkbox" /><span><strong data-i18n="llm.enableLabel">Enable LLM</strong><small data-i18n="llm.enableHint">Allow the bot to call OpenAI-compatible model services.</small></span></label>
|
||
<label class="toggle-card" for="llm-allow-net"><input id="llm-allow-net" name="llm_allow_net" type="checkbox" /><span><strong data-i18n="llm.netLabel">Enable LLM outbound requests</strong><small data-i18n="llm.netHint">When off, no LLM client is initialized and the bot will not call any external model service.</small></span></label>
|
||
</div>
|
||
<div class="field-grid llm-fields">
|
||
<div class="field"><label for="llm-base-url">LLM Base URL <span id="llm-url-required" class="optional-mark" data-i18n="common.optional">Optional</span></label><input id="llm-base-url" name="llm_base_url" type="url" inputmode="url" autocomplete="url" placeholder="https://api.example.com/v1" /><small data-i18n="llm.urlHint">HTTPS is required when LLM is enabled.</small></div>
|
||
<div class="field"><label for="llm-model"><span data-i18n="llm.modelLabel">Model name</span> <span id="llm-model-required" class="optional-mark" data-i18n="common.optional">Optional</span></label><input id="llm-model" name="llm_model" type="text" autocomplete="off" data-i18n-placeholder="llm.modelPh" placeholder="Leave blank for null" /></div>
|
||
<div class="field field-wide"><label for="llm-api-key">LLM API Key <span id="llm-key-required" class="optional-mark" data-i18n="common.optional">Optional</span></label><input id="llm-api-key" name="llm_api_key" type="password" autocomplete="new-password" spellcheck="false" data-i18n-placeholder="llm.keyPh" placeholder="Required when LLM is enabled; never echoed" /><small id="secret-marker-llm_api_key" class="secret-marker" role="status" aria-live="polite"></small></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="form-section" aria-labelledby="timezone-title">
|
||
<div class="form-section-heading"><span class="section-number">04</span><div><h3 id="timezone-title" data-i18n="timezone.title">Notification display</h3><p data-i18n="timezone.desc">Received times in Telegram notifications are rendered in this time zone.</p></div></div>
|
||
<div class="field-grid">
|
||
<div class="field"><label for="timezone">Time zone <span class="required-mark" data-i18n="common.required">Required</span></label><select id="timezone" name="timezone" required></select><small data-i18n="timezone.hint">Only fixed-offset zones are supported; DST is not tracked.</small></div>
|
||
</div>
|
||
</section>
|
||
</fieldset>
|
||
|
||
<div class="form-footer">
|
||
<p id="business-save-state" class="save-state" aria-live="polite" data-i18n="business.saveStateInitial">You can fill in the full configuration after the admin session is verified.</p>
|
||
<div class="form-actions"><button id="business-preflight-button" class="button button-quiet" type="button" disabled data-i18n="business.preflight">Test connection</button><button id="business-save-button" class="button button-primary" type="submit" disabled><span class="button-label" data-i18n="business.save">Save business config and hot-reload</span><span class="button-spinner" aria-hidden="true"></span></button></div>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="panel config-section callback-section" aria-labelledby="callback-title">
|
||
<div class="section-heading">
|
||
<div><p class="eyebrow" data-i18n="callback.eyebrow">External callbacks</p><h2 id="callback-title" data-i18n="callback.title">Register Telegram and Stalwart callbacks</h2></div>
|
||
</div>
|
||
<p class="section-copy" data-i18n="callback.copy">Save the business configuration first. This page registers callbacks with the Cloudflare Worker front proxy when it is enabled, or with the backend directly when it is not. Enter the public HTTPS origin that receives callbacks, then register both services from this page.</p>
|
||
<div class="field-grid">
|
||
<div class="field field-wide"><label for="callback-origin" data-i18n="callback.originLabel">Public HTTPS origin</label><input id="callback-origin" type="url" inputmode="url" autocomplete="url" placeholder="https://mail.example.com" data-i18n-placeholder="callback.originPlaceholder"/><small data-i18n="callback.originHint">Callbacks hit the Cloudflare Worker front proxy, or the backend directly when no proxy is enabled. Use the Worker URL when the proxy is enabled, otherwise the backend URL. Enter only the origin, without a path.</small></div>
|
||
</div>
|
||
<div class="form-footer"><p id="callback-state" class="save-state" aria-live="polite" data-i18n="callback.state">Registration is an explicit action and can be repeated safely. Registering a new address cancels the previous subscription, so the old address stops receiving pushes.</p><div class="form-actions"><button id="callback-register-button" class="button button-primary" type="button" disabled data-i18n="callback.register">Register callbacks</button></div></div>
|
||
</section>
|
||
|
||
<section class="panel config-section runtime-section" aria-labelledby="runtime-title">
|
||
<div class="section-heading">
|
||
<div><p class="eyebrow" data-i18n="runtime.eyebrow">Outbound requests</p><h2 id="runtime-title" data-i18n="runtime.title">Timeouts and retries</h2></div>
|
||
<span class="persist-label"><span class="persist-dot"></span><span data-i18n="runtime.persist">Stored in Redis</span></span>
|
||
</div>
|
||
<p class="section-copy"><span data-i18n="runtime.bodyPre">Runtime parameters are read by </span><code>GET /api/config</code><span data-i18n="runtime.bodyMid"> and updated by </span><code>PUT /api/config</code><span data-i18n="runtime.bodyPost">; new values apply immediately to subsequent requests after saving.</span></p>
|
||
<form id="runtime-form" novalidate>
|
||
<fieldset id="runtime-fields" disabled>
|
||
<legend class="visually-hidden" data-i18n="legend.runtime">Runtime parameters</legend>
|
||
<div class="settings-grid">
|
||
<article class="setting-card"><label for="jmap-timeout" data-i18n="runtime.jmapLabel">JMAP timeout</label><p class="setting-description" data-i18n="runtime.jmapDesc">Upper wait limit for a single JMAP request.</p><div class="number-control"><input id="jmap-timeout" name="jmap_timeout_ms" type="number" min="100" max="300000" step="1" required /><span data-i18n="common.ms">ms</span></div><small data-i18n="runtime.jmapHint">100–300,000; default 15,000.</small></article>
|
||
<article class="setting-card"><label for="telegram-timeout" data-i18n="runtime.tgLabel">Telegram timeout</label><p class="setting-description" data-i18n="runtime.tgDesc">Upper wait limit for a single Telegram request.</p><div class="number-control"><input id="telegram-timeout" name="telegram_timeout_ms" type="number" min="100" max="300000" step="1" required /><span data-i18n="common.ms">ms</span></div><small data-i18n="runtime.tgHint">100–300,000; default 10,000.</small></article>
|
||
<article class="setting-card"><label for="llm-timeout" data-i18n="runtime.llmLabel">LLM timeout</label><p class="setting-description" data-i18n="runtime.llmDesc">Upper wait limit for a single model request.</p><div class="number-control"><input id="llm-timeout" name="llm_timeout_ms" type="number" min="100" max="300000" step="1" required /><span data-i18n="common.ms">ms</span></div><small data-i18n="runtime.llmHint">100–300,000; default 30,000.</small></article>
|
||
<article class="setting-card"><label for="max-retries" data-i18n="runtime.retriesLabel">Max retries</label><p class="setting-description" data-i18n="runtime.retriesDesc">Extra attempts after a failure; 0 means no retry.</p><div class="number-control"><input id="max-retries" name="max_retries" type="number" min="0" max="5" step="1" required /><span data-i18n="common.times">times</span></div><small data-i18n="runtime.retriesHint">0–5; default 3.</small></article>
|
||
</div>
|
||
</fieldset>
|
||
<div class="form-footer">
|
||
<p id="runtime-save-state" class="save-state" aria-live="polite" data-i18n="runtime.saveStateInitial">Runtime parameters become editable after a successful read.</p>
|
||
<div class="form-actions"><button id="defaults-button" class="button button-quiet" type="button" disabled data-i18n="runtime.defaults">Restore defaults</button><button id="runtime-save-button" class="button button-primary" type="submit" disabled><span class="button-label" data-i18n="runtime.save">Save runtime parameters</span><span class="button-spinner" aria-hidden="true"></span></button></div>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
</section>
|
||
|
||
<aside id="privacy-note" class="privacy-note" hidden><span class="privacy-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 3 5 6v5c0 4.5 2.8 8 7 10 4.2-2 7-5.5 7-10V6l-7-3Z"/><path d="m9 12 2 2 4-4"/></svg></span><p><strong data-i18n="privacy.title">Secrets are never echoed or persisted to the browser.</strong> <span data-i18n="privacy.body">Your settings are encrypted before they are saved on the server. Email contents are not shown here.</span></p></aside>
|
||
</main>
|
||
</body>
|
||
</html>
|