Commit Graph
7 Commits
Author SHA1 Message Date
Logan CusanoandClaude Opus 5.5 8af410aede Discord: several radio streams per voice channel; adding never overwrites (#213)
Owner-reported on live: adding a radio channel for a server + voice channel
that already had one (different label, system, even bot) overwrote the
existing stream and made its bot leave, because stream_id was guild:channel.

- New streams get guild:channel:<12 hex> ids (leases.new_stream_id); old
  guild:channel docs keep working for read/stop/remove/node reports. Route
  validators accept both (leases.STREAM_ID_PATTERN).
- start_stream: no stream_id = ADD. Same system already on in that channel
  is idempotent (already_on, label/mode untouched); a stopped one of that
  system is restarted; anything else is a new doc. stream_id = explicit
  restart/edit (admin Start button, reconciler), incl. moving to another bot.
- preferred_token_id is strict: bot already in this server -> 409
  preferred_busy naming the stream; on another node -> preferred_other_node;
  unusable -> preferred_unavailable. No silent swap. pool_exhausted now says
  one-bot-per-server and to add + invite another bot.
- One-token-per-guild was already enforced inside the lease transaction;
  now covered by race tests. Per-guild asyncio lock serialises the
  already-on / server-cap check with the create (guild half of #223).
- Stop by stream_id (POST /discord/guilds/{id}/stop {stream_id}); admin UI
  Stop/Start/Remove act on the row's stream; node picker lists every stream
  in a channel and adds beside other systems.
- Bot: /leave stream: (autocomplete) stops one; /leave channel: with several
  streams asks which unless all:True; bare /leave still stops the server.
  /status and /join name each stream's bot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:30:40 -04:00
Logan CusanoandClaude Opus 5.5 25553fbac6 Discord remove server: audit a removal a restart interrupted
DELETE /discord/guilds/{id} deletes channels one by one. If a channel was
restarted mid-removal, delete_stream raised "restarted" and the route
returned 409 with no audit entry, though the channels before it were
already deleted for good. The error now carries the channels already
removed and the route writes discord.guild.remove with outcome
"incomplete" before returning the 409. The server stays linked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:48:30 -04:00
Logan CusanoandClaude Opus 5.5 c92bdfe841 Discord: log node-reported stream state; remove a radio channel or a server
- handle_actual logs every node-reported state TRANSITION (not heartbeats)
  with stream/node/guild/channel/token ids and the node's error text: info
  for connected/connecting/stopped, warning for error, and a dedicated
  "radio bot X failed to log in on node N: <reason>" line for login_failed.
  The node's reason code is now stored in actual.code and shown next to the
  error on Settings > Discord and the node page.
- Remove a radio channel permanently: streams.delete_stream stops it through
  desired state first (node republished without it, token -> releasing per
  #216, freed on the node's ack), then deletes the doc in a transaction that
  refuses if the channel was restarted in between. DELETE
  /discord/streams/{id} (platform admin) and DELETE
  /discord/nodes/{node}/streams/{id} (admin or org owner, same org + node
  only; operators can still stop, not remove). Audited.
- Remove a server: DELETE /discord/guilds/{id} (admin) deletes every radio
  channel in it the same way, then tombstones discord_guilds (org_id None +
  unlinked_at, #218 semantics, plus removed_at) so it never falls back to
  DISCORD_UNLINKED_GUILD_ORG_ID; the admin page hides it, and /drb link or the
  admin link brings it back. Optional leave_radio_bots makes pool bots and the
  server's org's own BYO bots leave via Discord REST; the DRB server bot stays
  until kicked, and the UI says so. Audited.
- UI: Remove (with confirm) on Radio channels, Servers and the node page list.

Refs #213, #221

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:40:43 -04:00
Logan CusanoandClaude Opus 5.5 455c55f5c2 feat(discord): self-serve server->org linking with /drb link (#213 item 1)
- c2-core: POST /discord/link-codes (org owner/admin) mints a single-use,
  10-minute code; minting revokes the org's older codes. The bot redeems it
  via POST /discord/guilds/{id}/link/redeem (service key) inside one
  Firestore transaction, so two servers racing for one code cannot both win
  and a server linked to org A refuses org B's code. GET/DELETE
  /discord/linked-guilds lists/unlinks the caller's org's servers; unlink
  stops that server's streams. Rate-limited (mint per user, redeem per
  server and per Discord user); every mint/link/refusal/unlink audited.
- resolve_guild_org: an explicitly unlinked server no longer falls back to
  DISCORD_UNLINKED_GUILD_ORG_ID. Linking away from the fallback org stops
  the streams it had running for that org.
- Discord bot: /drb link code: (Manage Server, enforced at runtime too);
  GatedTree answers every other command in an unlinked server with how to
  link (autocomplete returns nothing). With the default fallback org set,
  nothing is gated — unchanged behaviour.
- Frontend: "Linked Discord servers" card on Settings -> Organization —
  generate code with countdown + copy, polls for the new link, unlink.
- 16 tests: single-use, two-server race, one-server/two-org race, expiry,
  revoked/malformed codes, wrong-org takeover, idempotent relink, rate
  limits, fallback stop, scoped list, unlink without fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:01:25 -04:00
Logan CusanoandClaude Opus 5.5 03ba7eacd9 Discord leases: free releasing tokens on gone nodes; stamp before building desired list
- reconcile_once only walked desired=on streams, so a token left "releasing"
  by /leave while its node was offline (or a deleted node) never returned to
  the pool until that node came back (server-26#204 leak shape). Sweep leased
  tokens whose node is offline/deleted past the lease grace.
- publish_desired stamped release_seq AFTER building the list, so a stop
  landing between the two got a seq whose list still held the stream; an ack
  for it could free a token the node was still told to use. Stamp first.

Three regression tests; each fails on the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:16:31 -04:00
Logan CusanoandClaude Opus 5.5 a31a8df17c Discord radio channels: wire leases + desired-state streams (server-26#203, #204)
c2-core
- discord_streams/discord_leases wired in: POST /discord/streams,
  POST /discord/guilds/{id}/stop, GET /discord/guilds/{id}/overview,
  GET /discord/streams/{id}, PUT /discord/guilds/{id}/link (admin),
  GET /discord/node/{node}/tokens/{id} (node key, lease holder only).
- Retained nodes/{id}/discord/desired (token ids, never token strings),
  nodes/+/discord/actual subscribed and handled; per-node publish lock keeps
  the retained list the newest; failed publishes retried by the reconciler.
- Tokens freed only after the node acks logout (or offline past
  discord_lease_offline_release_seconds) - sweeper and C2 startup no longer
  release leases.
- /nodes/{id}/command discord_join/leave map onto streams; CommandPayload
  keeps preferred_token_id. /tokens: admin-only list, validate on add against
  Discord, refresh, flush via transactional force release.
- Guild -> org via discord_guilds, unlinked guilds fall back to
  DISCORD_UNLINKED_GUILD_ORG_ID (default "founding").
- tests/fake_firestore.py: optimistic-transaction fake; 27 lease/race tests.

discord bot
- /join, /leave, /status, autocomplete scoped to the calling guild; /leave
  never touches another server; Manage Channels default permission;
  /join edits its reply with the node's real result; /help copy fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:05:26 -04:00
Logan CusanoandClaude Opus 5.5 0a724a064f WIP: discord system — transactional token leases + desired-state streams (c2-core, unwired)
Paused by owner instruction; not wired into routers/mqtt yet, untested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:55:09 -04:00