ui: drop the build-version display and fix the callback card

The value rendered was nogit+<UTC build time>, which carried no
information for the operator, so remove all three display sites
(topbar next to the secure-channel note, setup card, page footer).
Clears the three querySelectors, renderVersion, and the two dead i18n
keys. /api/status is untouched and still returns version - the API is
now the only way to confirm a deploy landed.

The <footer> element is removed with its span rather than left empty:
it had no other child, and an empty footer would still render a
visible separator band (border-top plus 41px padding).

The callback section's field was the only one in the SPA sitting
directly on the panel, with no padded ancestor. It spanned the full
card width with zero side padding and pressed the button bar, while
the copy above it was indented 25px. Wrap it in .field-grid and pad it
to match its card siblings on both desktop and the 720px breakpoint;
.field-wide's grid-column now has a grid ancestor.

Rewrite the callback copy to name the Cloudflare Worker front proxy
explicitly instead of the bare word Worker, and state that registering
a new address retires the previous subscription.
This commit is contained in:
anonsaber 2026-10-01 11:17:50 +08:00
parent b3df7c5d87
commit 0b27ca03a6
3 changed files with 16 additions and 40 deletions

View File

@ -147,8 +147,6 @@
"privacy.body": "Your settings are encrypted before they are saved on the server. Email contents are not shown here.",
"common.required": "Required",
"common.optional": "Optional",
"common.buildVersion": "Build version",
"common.buildVersionUnknown": "Build version unavailable",
"common.atLeastOne": "At least one",
"common.ms": "ms",
"common.times": "times",
@ -210,11 +208,11 @@
"login.fail": "Unable to connect to the admin service; check the network and retry.",
"callback.eyebrow": "External callbacks",
"callback.title": "Register Telegram and Stalwart callbacks",
"callback.copy": "Save the business configuration first. Enter the public HTTPS origin that receives callbacks, then register both services from this page.",
"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.",
"callback.originLabel": "Public HTTPS origin",
"callback.originPlaceholder": "https://mail.example.com",
"callback.originHint": "Use the Worker URL when enabled; otherwise use the backend URL. Enter only the origin, without a path.",
"callback.state": "Registration is an explicit action and can be repeated safely.",
"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.",
"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.",
"callback.saveFirst": "Save the current business configuration before registering callbacks.",
"callback.register": "Register callbacks",
"callback.registerBusy": "Registering…",
@ -360,8 +358,6 @@
"privacy.body": "你的设置会加密后保存在服务器上。这里不会显示邮件内容。",
"common.required": "必填",
"common.optional": "可选",
"common.buildVersion": "构建版本",
"common.buildVersionUnknown": "构建版本未知",
"common.atLeastOne": "至少一项",
"common.ms": "毫秒",
"common.times": "次",
@ -425,11 +421,11 @@
"timezone.desc": "Telegram 通知中的收件时间按此时区渲染。",
"callback.eyebrow": "外部回调",
"callback.title": "注册 Telegram 和 Stalwart 回调",
"callback.copy": "先保存业务配置,再输入接收回调的公开 HTTPS 地址。本页会向两个服务注册回调。",
"callback.copy": "先保存业务配置。本页会向 Cloudflare Worker 前置代理(启用时)或后端本身注册回调。请输入接收回调的公开 HTTPS 地址,然后在本页注册两个服务。",
"callback.originLabel": "公开 HTTPS 地址",
"callback.originPlaceholder": "https://mail.example.com",
"callback.originHint": "启用 Worker 时填写 Worker 地址,否则填写后端地址。只填写站点 origin,不带路径。",
"callback.state": "注册需要显式操作,可以安全地重复执行。",
"callback.originHint": "回调会打到 Cloudflare Worker 前置代理或直接打到后端。启用前置代理时填 Worker 地址,否则填后端地址;只填站点 origin,不带路径。",
"callback.state": "注册需要显式操作,可以安全地重复执行。改用新地址注册会先注销旧订阅,旧地址不会再收到推送。",
"callback.saveFirst": "请先保存当前业务配置,再注册回调。",
"callback.register": "注册回调",
"callback.registerBusy": "正在注册…",
@ -619,9 +615,6 @@
const llmModel = document.querySelector("#llm-model");
const notice = document.querySelector("#notice");
const preflightButton = document.querySelector("#business-preflight-button");
const versionFooter = document.querySelector("#build-version");
const versionTop = document.querySelector("#build-version-top");
const versionSetup = document.querySelector("#build-version-setup");
// C-NO-LOCAL-WRITE / C-REDIS-ONLY-STATE: credentials and configuration stay in page memory.
let adminSession = "";
@ -716,24 +709,12 @@
return parseResponseText(responseText, response.status);
}
// The build fingerprint comes from /api/status so the SPA never hard-codes one: the binary
// supplies it at compile time and the page just echoes it. Absent means the backend is older
// than the version endpoint, which is itself useful information for an operator.
function renderVersion(version) {
const value = typeof version === "string" && version.trim() ? version.trim() : "";
const label = t("common.buildVersion");
if (versionFooter) versionFooter.textContent = value ? `${label} ${value}` : t("common.buildVersionUnknown");
if (versionTop) versionTop.textContent = value;
if (versionSetup) versionSetup.textContent = value ? `${label} ${value}` : "";
}
async function showSetupStatus(options = {}) {
showSetupView(t("setup.checking"));
statusRetryButton.disabled = true;
statusRetryButton.textContent = t("setup.checkingBtn");
try {
const status = await request("/api/status", "GET", undefined, "");
renderVersion(status && status.version);
if (status && status.ready === true) {
setupMode = false;
setupCard.hidden = true;

View File

@ -19,7 +19,6 @@
<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>
<span id="build-version-top" class="topbar-version" aria-hidden="true"></span>
</header>
<main class="layout">
@ -43,7 +42,6 @@
<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>
<p id="build-version-setup" class="build-version" aria-hidden="true"></p>
</section>
<section id="auth-card" class="panel auth-panel" aria-labelledby="auth-title" hidden>
@ -137,13 +135,15 @@
</form>
</section>
<section class="panel config-section" aria-labelledby="callback-title">
<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. Enter the public HTTPS origin that receives callbacks, then register both services from this page.</p>
<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">Use the Worker URL when enabled; otherwise use the backend URL. Enter only the origin, without a path.</small></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.</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>
<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">
@ -172,7 +172,5 @@
<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>
<footer class="footer"><span id="build-version" class="footer-version">build version —</span></footer>
</body>
</html>

View File

@ -31,8 +31,6 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
.brand-copy strong { font-size: 14px; letter-spacing: -.2px; }
.brand-copy > span { color: #85869a; font-size: 11px; letter-spacing: .05em; }
.topbar-note { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; color: #77798e; font-size: 12px; }
.topbar-version { display: none; color: #a1a1b0; font-size: 10px; padding: 4px 8px; border: 1px solid #e6e6ef; border-radius: 999px; background: rgba(255, 255, 255, .8); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
@media (min-width: 900px) { .topbar-version { display: inline-block; } }
.secure-dot, .persist-dot { width: 7px; height: 7px; border-radius: 50%; background: #48ae86; box-shadow: 0 0 0 3px rgba(72, 174, 134, .12); }
.layout { width: min(960px, calc(100% - 40px)); margin: 0 auto; padding: 54px 0 56px; }
@ -65,7 +63,6 @@ h3 { margin-bottom: 4px; color: #35364e; font-size: 15px; }
.setup-panel .panel-copy { margin-bottom: 14px; }
.setup-missing { margin: 0 0 17px; padding: 12px 14px; border: 1px solid #f2e9c8; border-radius: 8px; background: #fffbef; color: #796132; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
.setup-privacy { margin: 0 0 17px; color: #858697; font-size: 12px; line-height: 1.65; }
.build-version { margin: 20px 0 0; padding-top: 15px; border-top: 1px solid #efeff5; color: #a1a1b0; font-size: 10px; line-height: 1.5; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
code { padding: 2px 5px; border-radius: 4px; background: #f1f0fa; color: #534a9c; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .92em; }
.auth-form > label, .field > label, .setting-card > label { display: block; margin-bottom: 7px; color: #47485f; font-size: 12px; font-weight: 700; }
.token-row { display: flex; gap: 10px; }
@ -144,6 +141,9 @@ fieldset:disabled .field input, fieldset:disabled .field textarea, fieldset:disa
.save-state.is-dirty { color: #9c712e; }
.form-actions { display: flex; gap: 8px; }
.runtime-section .settings-grid { padding: 0 25px 22px; }
/* The callback card has no `.form-section` wrapper, so its grid needs the
section padding the other sections get for free. */
.callback-section .field-grid { padding: 0 25px 22px; }
.settings-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.setting-card { min-width: 0; padding: 16px; border: 1px solid #eeeef4; border-radius: 11px; background: #fff; }
.setting-card > label { margin-bottom: 5px; color: #35364e; font-size: 13px; }
@ -158,9 +158,6 @@ fieldset:disabled .field input, fieldset:disabled .field textarea, fieldset:disa
.privacy-icon svg { width: 18px; height: 18px; }
.privacy-note p { margin: 0; font-size: 11px; line-height: 1.65; }
.privacy-note strong { color: #64657a; font-weight: 680; }
.footer { width: min(1100px, calc(100% - 48px)); display: flex; justify-content: center; margin: 0 auto; padding: 18px 0 23px; border-top: 1px solid rgba(224, 224, 238, .72); color: #a5a5b2; font-size: 9px; letter-spacing: .08em; }
.footer span:last-child { letter-spacing: 0; }
.footer-version { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0; }
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; clip-path: inset(50%); border: 0; }
@media (max-width: 720px) {
@ -188,7 +185,7 @@ fieldset:disabled .field input, fieldset:disabled .field textarea, fieldset:disa
.form-actions .button { flex: 1; }
.runtime-section .settings-grid { padding: 0 14px 17px; }
.section-copy { margin: -3px 17px 16px; }
.footer { width: calc(100% - 34px); }
.callback-section .field-grid { padding: 0 17px 17px; }
}
@media (max-width: 420px) {