aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api/admin.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-12 13:48:09 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-12 16:36:54 +0200
commit98e27c8f251022320020716a9ef7a5b892ad2b61 (patch)
tree7cfb3f397c3f001768413676a739fe147743c824 /packages/meshbay-hub/src/meshbay_hub/api/admin.py
parente671b931fd594a39fc840916c81b5d4b1f1e3227 (diff)
downloadmeshbay-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/api/admin.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/admin.py59
1 files changed, 55 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/admin.py b/packages/meshbay-hub/src/meshbay_hub/api/admin.py
index 219e8a9..087c221 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/admin.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/admin.py
@@ -38,12 +38,23 @@ class GroupPatchRequest(BaseModel):
class SettingsPatchRequest(BaseModel):
allow_public_groups: bool | None = None
+ # Every mail bound, each optional: the panel sends only what changed.
+ mail: dict[str, int] | None = None
# ── Instance settings ────────────────────────────────────────────────────────
-def _settings_payload(allow_public_groups: bool) -> dict:
- return {"allow_public_groups": allow_public_groups}
+def _settings_payload(allow_public_groups: bool, mail: dict) -> dict:
+ return {
+ "allow_public_groups": allow_public_groups,
+ "mail": mail,
+ # So the panel can show what a field falls back to, and label the
+ # bounds it will refuse — rather than the operator finding out by
+ # having a value silently clamped.
+ "mail_defaults": {k: hub_settings.mail_default(k)
+ for k in hub_settings.MAIL_KEYS},
+ "mail_bounds": {k: list(v) for k, v in hub_settings.MAIL_BOUNDS.items()},
+ }
@router.get("/settings")
@@ -52,7 +63,9 @@ async def admin_get_settings(
db: AsyncSession = Depends(get_db),
):
"""Instance-wide policy an admin controls from the panel. Moderators may read."""
- return _settings_payload(await hub_settings.public_groups_allowed(db))
+ return _settings_payload(
+ await hub_settings.public_groups_allowed(db),
+ await hub_settings.mail_limits(db))
@router.patch("/settings")
@@ -82,7 +95,45 @@ async def admin_patch_settings(
))
await db.commit()
- return _settings_payload(await hub_settings.public_groups_allowed(db))
+ if body.mail:
+ unknown = sorted(set(body.mail) - set(hub_settings.MAIL_KEYS))
+ if unknown:
+ raise HTTPException(
+ status_code=422, detail=f"Unknown mail setting(s): {unknown}")
+ changed = []
+ for key, value in body.mail.items():
+ clamped = hub_settings.clamp_mail_value(key, value)
+ await hub_settings.set_raw(db, f"mail.{key}", str(clamped))
+ changed.append(f"{key}={clamped}")
+ log.info("Mail bounds changed by %s: %s",
+ current_user.username, ", ".join(changed))
+ db.add(IPLog(
+ user_id=current_user.id,
+ event="admin_mail_limits_update",
+ ip_address="admin",
+ detail=", ".join(changed)[:255],
+ ))
+ await db.commit()
+
+ return _settings_payload(
+ await hub_settings.public_groups_allowed(db),
+ await hub_settings.mail_limits(db))
+
+
+@router.get("/mail")
+async def admin_mail_status(
+ current_user: User = Depends(require_moderator),
+ db: AsyncSession = Depends(get_db),
+):
+ """Is the hub still sending, and how much of the hour is left.
+
+ There was no way to see this at all: a refusal was a line in the journal,
+ so an instance that had stopped sending registration codes looked, from the
+ panel, exactly like one that had no sign-ups.
+ """
+ from meshbay_hub import mail
+
+ return await mail.status(db)
# ── Stats ────────────────────────────────────────────────────────────────────