aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/admin-page.js
diff options
context:
space:
mode:
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.js78
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`