diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-12 13:48:09 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-12 16:36:54 +0200 |
| commit | 98e27c8f251022320020716a9ef7a5b892ad2b61 (patch) | |
| tree | 7cfb3f397c3f001768413676a739fe147743c824 /packages/meshbay-hub/src/meshbay_hub/static/admin-page.js | |
| parent | e671b931fd594a39fc840916c81b5d4b1f1e3227 (diff) | |
| download | meshbay-98e27c8f251022320020716a9ef7a5b892ad2b61.tar.gz | |
feat(hub): the mail bounds are settings, with a panel to change them
They were constants in two modules, so an operator could not touch them
without editing code and redeploying — and the hour a budget runs out is not
when anyone wants to do that.
`[mail]` in hub.toml carries the defaults; the live values live in
`hub_settings`, read at each use. A missing row falls back to what the
configuration file says, so an instance that never opens the panel behaves as
its file describes. The panel sends only what changed, the hub clamps each
value to a stated range and refuses a key it does not know, and the response
is what gets rendered — so a clamped value is never shown as stored.
`GET /v1/admin/mail` is the other half. There was no way to see any of this:
a refusal was a line in the journal, so an instance that had stopped sending
sign-up codes looked, from the panel, exactly like one with no sign-ups. It
reports the hour's use, what is left for sign-ups, and what is left for
recovery — the difference between those two being the reserved share made
visible.
Labels in all ten catalogues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/admin-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/admin-page.js | 78 |
1 files changed, 77 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js b/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js index dbe26ab..3cc42db 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js @@ -12,6 +12,10 @@ export function AdminPage({ token, role }) { const [stats, setStats] = useState(null); const [settings, setSettings] = useState(null); const [settingsSaving, setSettingsSaving] = useState(false); + const [mailStatus, setMailStatus] = useState(null); + // Edited values live here until Save, so a half-typed number is never sent + // and a rejected one never looks applied. + const [mailDraft, setMailDraft] = useState(null); const [users, setUsers] = useState([]); const [usersTotal, setUsersTotal] = useState(0); const [userSearch, setUserSearch] = useState(''); @@ -38,7 +42,11 @@ export function AdminPage({ token, role }) { try { const data = await hubFetch('/v1/admin/settings', { token }); setSettings(data); + setMailDraft({ ...data.mail }); } catch (e) { setError(e.message); } + try { + setMailStatus(await hubFetch('/v1/admin/mail', { token })); + } catch { /* the panel still works without the live figure */ } }, [token]); const saveSettings = useCallback(async (patch) => { @@ -50,6 +58,14 @@ export function AdminPage({ token, role }) { const data = await hubFetch('/v1/admin/settings', { method: 'PATCH', body: patch, token }); setSettings(data); + // The hub clamps what it was given, so the draft is reset from the + // answer rather than left showing a number that was not stored. + setMailDraft({ ...data.mail }); + if (patch.mail) { + try { + setMailStatus(await hubFetch('/v1/admin/mail', { token })); + } catch { /* leave the previous figure rather than blanking it */ } + } } catch (e) { setError(e.message); } finally { setSettingsSaving(false); } }, [token]); @@ -160,7 +176,20 @@ export function AdminPage({ token, role }) { } catch (e) { setError(e.message); } }, [token]); - const TABS = ['general', 'stats', 'users', 'groups', 'nodes', 'logs', 'blocklist']; + // The order the fields are shown in, and the only keys the panel will send. +// The hub refuses anything outside its own list as well — a form is not where +// that decision belongs. +const MAIL_FIELDS = [ + 'hourly_budget', + 'hourly_reserved_for_recovery', + 'destination_daily_cap', + 'destination_cooldown_seconds', + 'verification_resend_cooldown', + 'reset_cooldown', + 'email_change_cooldown', +]; + +const TABS = ['general', 'stats', 'users', 'groups', 'nodes', 'logs', 'blocklist']; const canEditSettings = role === 'admin'; return html` @@ -191,6 +220,53 @@ export function AdminPage({ token, role }) { ${!canEditSettings && html` <p class="settings-hint">${t('admin.settings_readonly')}</p>`} </div> + + <div class="settings-section"> + <h3 class="settings-heading">${t('admin.mail_heading')}</h3> + <p class="settings-hint">${t('admin.mail_hint')}</p> + + ${mailStatus && html` + <div class="settings-row"> + <span class="settings-label">${t('admin.mail_this_hour')}</span> + <span>${mailStatus.hourly_used} / ${mailStatus.hourly_budget}</span> + </div> + <p class="settings-hint"> + ${t('admin.mail_remaining', { + general: mailStatus.general_remaining, + recovery: mailStatus.recovery_remaining, + })} + </p> + `} + + ${mailDraft && MAIL_FIELDS.map(key => html` + <div class="settings-row" key=${key}> + <span class="settings-label">${t('admin.mail_' + key)}</span> + <input type="number" class="settings-number" + min=${(settings.mail_bounds?.[key] || [0])[0]} + max=${(settings.mail_bounds?.[key] || [0, 0])[1]} + value=${mailDraft[key]} + disabled=${!canEditSettings || settingsSaving} + onInput=${e => setMailDraft(d => ({ ...d, [key]: e.target.value }))} /> + </div> + `)} + + ${canEditSettings && mailDraft && html` + <div class="settings-row"> + <button class="btn" disabled=${settingsSaving} + onClick=${() => saveSettings({ + mail: Object.fromEntries(MAIL_FIELDS + // Only what changed, and only what is a number: an empty + // field is someone mid-edit, not a request to set zero. + .filter(k => mailDraft[k] !== '' && mailDraft[k] !== null + && Number(mailDraft[k]) !== settings.mail[k]) + .map(k => [k, Number(mailDraft[k])])), + })}>${t('admin.mail_save')}</button> + <button class="btn btn-secondary" disabled=${settingsSaving} + onClick=${() => setMailDraft({ ...settings.mail_defaults })} + >${t('admin.mail_reset_defaults')}</button> + </div> + `} + </div> `} ${tab === 'stats' && stats && html` |