<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/api/admin.py, branch 0.10</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.10</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.10'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-28T08:09:38Z</updated>
<entry>
<title>feat(hub): group names unique per owner, shown as name@owner</title>
<updated>2026-08-28T08:09:38Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-28T08:09:38Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=58ca879b017da62e77f40752d8d94fef3f315a1e'/>
<id>urn:sha1:58ca879b017da62e77f40752d8d94fef3f315a1e</id>
<content type='text'>
A group's identity stays its UUID. What changes is that "the name is
unique" — until now an unenforced expectation — becomes real, scoped to the
owner account, and the owner's username is surfaced so two groups called
"photos" on different nodes can be told apart.

Hub:
- `groups` gains a functional unique index `uq_groups_owner_name` on
  `(admin_id, lower(name))` (model + migration c3d4e5f6a7b8). The migration
  pre-flights: if the data already clashes it aborts and lists the
  offending (admin_id, name) pairs rather than renaming anyone's group.
  meshbay.org checked clean.
- `create_group` trims the name, rejects blank (422) and an owner-scoped
  case-insensitive clash (409), with an IntegrityError backstop for the
  race, and returns `owner_username`.
- `owner_username` added to `/v1/groups/mine`, `GET /v1/groups` (local
  rows), `POST /v1/groups/{id}/join`, `GET /v1/admin/groups`.

SPA:
- new `static/group-name.js` — `&lt;GroupName name owner [inline]&gt;` renders the
  name with the `@owner` handle on a smaller grey line under it.
- used in the sidebar, the group-page header, Explore cards, the Admin
  groups table, and cross-group Search (via a widened `cacheGroupIndex`
  carrying the owner). Delete/leave confirmations show `name@owner` inline.
- federated Explore rows show `@&lt;source_hub&gt;` instead of an account.

Design record and the locked decisions: ~/next/groupnames.md (out of repo).
MNP unchanged. Tests: test_group_name_unique.py, test_group_name_migration.py.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
</content>
</entry>
<entry>
<title>feat(hub): let a hub admin disable public groups instance-wide</title>
<updated>2026-08-28T00:51:00Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-28T00:51:00Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b5b4f188a39fc96c4d32e67151e067b1add6dcfc'/>
<id>urn:sha1:b5b4f188a39fc96c4d32e67151e067b1add6dcfc</id>
<content type='text'>
A new General tab in Administration carries one switch, allow_public_groups,
stored in a hub_settings key/value table (runtime-editable, unlike hub.toml).
Default is on; an absent row means on, so an upgrade changes nothing.

Enforcement is server-side on every hub-mediated path, not just the SPA:

  - create_group        refuses visibility=public (403), staff included
  - list_public_groups  the directory returns nothing (local + federated)
  - join_group          open-joining a public group is refused
  - group_online_nodes  a non-member of a public group is handed no node
  - signaling.webrtc_offer  drops the "node hosts an open group" fallback
  - federation.export_directory  advertises nothing to peer hubs

The switch is read live, so flipping it back restores every path. Existing
members of a group that predates the switch keep their membership row and
their access — this is plan A, not a purge. GET /v1/hub/info exposes the
flag (unauthenticated) so the create-group form and the sidebar's "Public
groups" link render correctly.

Also in the admin Groups tab: a Revoke action beside Suspend. Suspend is the
reversible hub flag; Revoke calls POST /v1/admin/revoke, which sets
status=revoked and broadcasts a signed revocation every node enforces
(denylist + dropped live sessions). It is confirm-guarded and names the group.

And a message fix the revoke work surfaced: group_online_nodes, join_group and
webrtc_offer answered "Group is suspended" for any non-active status. They now
report the real state, so a member of a revoked group is told "Group is
revoked" rather than something reversible-sounding.

Tests: test_public_groups_toggle.py (10) covers the switch end to end and the
five enforcement paths; test_revocation.py gains the status-message assertion.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
</content>
</entry>
<entry>
<title>fix: stop a stream on close, count only real users, record where a node is</title>
<updated>2026-08-15T17:01:08Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-15T17:01:08Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=05f4feab641740c944d636f29a03f8c0dd1328c7'/>
<id>urn:sha1:05f4feab641740c944d636f29a03f8c0dd1328c7</id>
<content type='text'>
**Closing the viewer left the node working.** Nothing told it to stop:
the player dropped its handlers, which only made the browser deaf. ffmpeg
kept running and held one of the node's two transcode slots until the
credit timeout expired two minutes later — which is why the next video
answered "server busy". `stream_stop` ends it at once, and the viewer
also drops its queue, ends the MediaSource and revokes the object URL on
the way out, any of which could be holding megabytes of decrypted video.

While there: `file_chunk` replies were matched to their requests by
arrival order, which was true by luck rather than by construction. The
reply now names the file it belongs to and is matched on that and the
chunk index; a chunk nobody is waiting for is dropped instead of being
handed to whatever request happens to be oldest.

**The administration panel counted its own history.** A deleted account
is tombstoned so the connection log stays readable, and every count and
list treated that row as a user — including a group's member count, and
the member list of the group itself. They do not any more.

**Where a node is.** `endpoint_hint` is what a node believes its address
to be, learned from a STUN server and sent to us: useful for reaching it,
and a claim. The announcement that carries it is signed with the node key
over a fresh timestamp, so the address that request *arrives from* is the
address of whoever holds that key — that is now recorded on the node row
and shown in a Nodes tab, next to the hint, with the difference spelled
out. Clients get the same treatment: `webrtc_offer` is logged with the
address the hub saw when a browser starts a peer connection.

Verified against the live deployment: the node's row reads 90.112.206.172
after a restart, and in e2e a stopped stream goes quiet in one message
and the next one starts immediately instead of being refused.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(logs): keep the username on records the account no longer answers for</title>
<updated>2026-08-14T22:50:05Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T22:50:05Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f4d04741379e77d2ef86cccc93856e590bfe1082'/>
<id>urn:sha1:f4d04741379e77d2ef86cccc93856e590bfe1082</id>
<content type='text'>
The connection log took the name from a join on `users`, and deletion
tombstones that row — so every record belonging to a deleted account
reported `deleted-3f9a1c`, which is the one answer that helps nobody. The
log is kept for a legal retention period precisely so it can say who did
what; losing the name at deletion kept the data and lost the point of it.

`ip_logs.username` is written as the account is erased, and stays NULL
while the account is alive, where the join is better because it cannot go
stale. The admin view prefers the stored name when there is one: the join
still answers after deletion, just with the tombstone.

Releasing the username for re-registration and keeping it in the log are
separate things, and the guide now says so.

On the node side, the pre-proof audit line records the username the
session already knew, instead of leaving the column empty.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(account): a user can delete their own account, an admin can delete one</title>
<updated>2026-08-14T20:41:02Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T20:41:02Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=8d8f85b4bf976249266a89f692408027216711b7'/>
<id>urn:sha1:8d8f85b4bf976249266a89f692408027216711b7</id>
<content type='text'>
Both go through the same erasure, so there is one description of what happens
rather than two that drift.

Gone: credentials, email, node key, group memberships, notifications, refresh
tokens, node registrations. The username is released.

Kept, on purpose and stated in the UI: the row itself, emptied, and the IP log
that points at it. Those logs exist for a year to answer legal requests, and a
log that can no longer say whose connection it recorded keeps the data while
losing the only thing it is for. So the account becomes a tombstone rather than a
hole in the table.

Out of reach, also stated: files uploaded to nodes, and the identity keys nodes
pinned. Those are on machines the hub does not command, and only their operators
can remove them — `member unpin` and a delete on their own disk. Saying so in the
confirmation matters more than the button.

Owning groups blocks deletion, with the list. Cascading would delete other
people's groups out from under them; the account holder can hand them over or
delete them first, deliberately.

Self-deletion re-checks the passphrase. A live token may be a borrowed laptop or
a tab left open, and it is not consent to something irreversible. Admin deletion
requires admin rather than moderator: suspension is the reversible moderation
tool and stays one click away.

A deleted account's access token stops working at once — the status check already
refuses anything but "active", which the tests now pin down, because refresh
tokens being gone would otherwise leave up to an hour of usable session.

Tests: 8 covering what survives and what does not, plus a db_session fixture for
assertions that cannot honestly be made through the API.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: Phase 12 — P2P crypto material, password split, node Ed25519 auth</title>
<updated>2026-08-13T01:56:30Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-13T01:56:30Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f0248975908ad670fa8a820f865bf22ea8d0172d'/>
<id>urn:sha1:f0248975908ad670fa8a820f865bf22ea8d0172d</id>
<content type='text'>
Baseline commit capturing in-progress Phase 12 work that was already present
in the working tree (uncommitted) before the Phase 11.5 security remediation
begins. Committed as-is, without review or modification, so that remediation
changes arrive as a separable diff.

Contents: BundleStore (P2P GEK + keypair bundles), password split
(auth_key / bundle_key), node Ed25519 auth (POST /v1/nodes/auth, node-scoped
JWT), GEK-HMAC handshake proof with DTLS channel binding, Ed25519 admin
challenge-response, node local admin UI rewrite, browser key persistence.

Not authored in this session — captured to establish a baseline.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(hub): Phase 10.5–10.8, 10.10 — notifications, settings, search, version</title>
<updated>2026-08-11T10:40:13Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-11T10:40:13Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=edde9e441fb6b84e9d56215d6e2a8d9338b8f962'/>
<id>urn:sha1:edde9e441fb6b84e9d56215d6e2a8d9338b8f962</id>
<content type='text'>
- 10.5: Notification model + CRUD API (list, mark read, mark all read)
  Triggered on: group invite, role change, suspend/unsuspend
- 10.6: SettingsPage shows role, per-group notification mute (localStorage)
- 10.7: GET /v1/groups?q= search filter (ilike on name)
- 10.8: NotificationFeed on home page + bell with unread badge in navbar
- 10.10: GET /v1/hub/version endpoint for client update checks
- 8 new tests (test_notifications.py), 155 total

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(hub): Phase 10.1–10.4 — Site overlay + admin/moderation UI</title>
<updated>2026-08-11T09:50:08Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-11T09:50:08Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c8f2de4025ea67b579e66cf608f08a8d35ee4a3c'/>
<id>urn:sha1:c8f2de4025ea67b579e66cf608f08a8d35ee4a3c</id>
<content type='text'>
- Site overlay: landing page, /about, /downloads (dark/light, responsive)
- User role column (user/moderator/admin) with config-based admin sync
- require_moderator dependency + admin API (8 endpoints: stats, users,
  groups, audit logs)
- Admin SPA panel at #/admin with 5 tabs (stats, users, groups, logs,
  blocklist) — visible only to moderators/admins
- SPA also served at /app/ for Caddy site overlay integration
- GET /v1/users/me returns current user role
- 15 new tests, 147 total passing

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
