diff options
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` |