bf206334e3eb9aa7a1ced7b93207670c210b1a16
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bf206334e3 |
fix(frontend): finish the token migration — raw grey classes -> theme tokens
UI_REDESIGN chunk 4 was never finished: 39 app/component files still used raw Tailwind greys, kept readable in light mode only by the !important override sheet. In dark mode text-gray-600 is ~2.3:1 on surface, and the light override maps it to #94a3b8 (~2.6:1 on white) — unreadable in both. Mechanical mapping (886 classes): gray-100/200 -> ink, 300/400 -> ink-2, 500/600/700 -> ink-muted, bg 950/900/800 -> page/surface/raised, border 800 -> line, 700/600 -> line-strong, plus hover/divide/placeholder. Alpha suffixes kept (tokens are color-mix, so /60 works). bg-gray-700, text-white and marketing pages left alone. text-[9px] -> text-[10px]. Frontend unverified: no Node on the authoring box (#207). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9c124d4c8b |
S8: main DRB bot optional end to end + owner-flow integration tests (server-26#225)
- Copy: picker, admin Servers/Streams/How-it-works, org Discord card, bot /help and the server-limit refusal no longer imply the DRB server bot is required; a radio bot alone is enough. - Admin overview: per-server main_bot_present, radio_bots, linked_via, directory_source; Servers tab gets a Bots column. - discord_web docstring: states the reversed "never calls Discord" rule (plan finding 5) and the optional server bot. - Server bot directory cog: re-push (which drops C2's cached permission answers) on overwrite changes, role permission changes and role deletes. - tests/test_discord_owner_flows.py: one-bot org (BYO bot, owner-set preferred, OAuth link, picker, start, reconciler) and full setup (server bot + 4 radio bots: placement, shared channel, every refusal explained), against a modelled Discord over httpx.MockTransport. - storage.download_audio removed: no caller on main since #233. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1ee1560383 |
Discord picker: per-bot permission status, owner's preferred bot (server-26#225 S4)
Node-page picker and the admin Add form now show, per voice channel, whether
a radio bot can stream there, from S3's voice-check routes:
- Badge per channel: Ready / Needs fix (names the missing permission) /
Couldn't check / Bots busy / No radio bot / Not a voice channel / Limit N.
- The chosen channel shows "<bot> can't Speak in #dispatch: <what removed it>"
plus c2-core's fix sentence, and an expandable per-bot list.
- Footer: "Permissions checked 40s ago · Re-check" (refresh=1, rate limited
server-side; a limited re-check says so). Invite links when no bot fits.
- Start is disabled only on a definite refusal (needs_fix, bad_channel);
unknown fails open. A channel this system already plays in is judged by
the bot that carries it, so Update/move is never blocked by the other bots.
- Admin Add form disables bots that are definitely denied in the bot list.
- 409 refusals (missing_permissions, preferred_busy, preferred_other_node,
bad_channel) are shown verbatim, multi-line.
Org owners (owner decision Q2) get "Radio bot per system" on
Settings > Organization: their own systems, their own BYO bots, set/clear
through S3's PUT /tokens/{id}/prefer/{system}. New GET /tokens/org/preferred
lists only the caller org's systems and bots (no pool bots, no token
strings; a pool bot an admin chose is flagged, not named).
The prefer route now drops the 5-minute doc_get_cached entry for every
system it rewrites, so a new preference applies to the next start.
Stacked on feat/225-s3-perm-check: must merge after S3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
ebd7e893ce |
Merge pull request 'Discord admin page + hide admin-only bot pool from operators (#213 items 2, 4)' (#219) from feat/213-discord-admin into main
Reviewed-on: #219 |
||
|
|
d3277c13e1 |
Discord admin page: servers, radio channels, radio bots (server-26#213 item 2)
Frontend, built from design-canvas/discord-system-mockup.html ("DRB admin"):
- Settings > Discord (/settings/discord, platform admin only; the tab is
hidden from org owners because every route behind it is admin-only).
KPIs + tabs:
* Radio channels: every discord_streams doc, desired -> actual, the node's
real error, red after 2 min desired-on without connecting. Stop
(POST /discord/guilds/{id}/stop), Start for a stopped channel and
"+ Add radio channel" (POST /discord/streams), with the invite links a
"no bot in this server" refusal carries.
* Servers: linked servers plus servers seen running radio while unlinked
(acting for DISCORD_UNLINKED_GUILD_ORG_ID); Link / Change org
(PUT /discord/guilds/{id}/link).
* Radio bots: valid / check_error, guild membership with a per-server
invite link for each server the bot is missing from, lease (node,
since, releasing), fan-out, Refresh (POST /tokens/{id}/refresh),
Remove, Release all leases (POST /tokens/flush), add with optional BYO org.
* How it works.
- /tokens now renders the same radio-bot table (new token fields + refresh).
- Left out, no backend yet: alert routes, incident forum/roles/plan columns,
per-channel talkgroup chips, "Generate link code" (item 1).
Backend (routers/discord.py):
- GET /discord/admin/overview (platform admin): guilds, streams with node
and bot names, orgs, systems, limits. No token strings, no Discord user
ids, no linking-admin uid.
- PUT /discord/guilds/{id}/link: digits-only id, optional guild_name label,
and re-linking a server to a different org stops its live channels
(the reconciler would otherwise keep re-placing them on the old org's
nodes, using its stored org_id).
- POST /discord/streams: guild_id/channel_id must be digits (the web form
types them in; the bot always sent snowflakes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
93fa3a6054 |
frontend: install command uses the node id from the mint form (node-26#4)
The mint panel's copy command hard-coded --node-id node-XXX. Now the label just entered (the operator types the node id there — placeholder relabeled "Node ID, e.g. node-003") is captured on mint and interpolated into the command: spaces → dashes, non [A-Za-z0-9_-] stripped (install.sh's rule), falling back to node-XXX only if that yields nothing. The "edit node-XXX" hint now only shows in the fallback case. Not typechecked (no node/npm here); one useState<string|null>, one derived string, a JSX conditional. `next build` in deploy.yml gates it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
0651bfe07a |
frontend: mint panel shows the full one-shot install command (node-26#4)
After a node enrollment token is minted, the panel now renders the paste-ready `curl -fsSL .../install.sh | sudo bash -s -- --token <minted> --node-id node-XXX --c2-url <derived> --mqtt-broker <derived>` line with a Copy button, alongside the bare token (also kept, also now copyable). - c2-url from NEXT_PUBLIC_C2_URL (same var lib/c2api.ts reads), fallback https://api.example.net - mqtt-broker derived as mqtt.<api-host minus leading api.> — a DNS assumption; the panel text tells the operator to check it - node id is a node-XXX placeholder; the panel collects none Pairs with node-26's install.sh (feat/one-shot-install). The raw/tag/v1/ URL resolves once v1 is re-cut at that PR's merge. NOT typechecked here (no node/npm in this environment); plain React, two useState booleans + one computed string, reviewed by eye. `next build` in the deploy workflow will catch a real type error before it ships. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
b7222230bd |
frontend: label machine-generated output and unbuilt entitlements (Gate A)
Gate A (BUSINESS_MODEL.md, board minutes #42, dated to today by minutes #79 decision 14) blocks putting a price or an unbuilt entitlement claim on a surface a reader can see, and requires that unverified machine assertions be labelled as such on the same screen as the assertion. The pricing leg was already met — /pricing and both homepage CTAs stopped quoting the invented catalog. Condition A2 was not: a search of the whole frontend for a "machine-generated" or "unverified" qualifier returned zero hits. Every transcript, summary, title, location, unit list and vehicle list is pipeline output that no human reviews, and entity-name accuracy in those transcripts has never been measured (server-26#48) — yet all of it was rendered to the reader as plain fact. Unqualified machine assertions about real incidents and real people is the exposure Gate A exists to stop. A2 — one reusable element, components/ui/MachineOutputNotice.tsx, rendered on the same screen as the output (a footnote elsewhere does not satisfy A1's "same screen" standard). Three variants for three shapes of surface, all saying the same thing; the "popup" variant uses fixed grays because a Leaflet popup is stock-white in both themes. Covered: - incident detail: under the summary (covers summary, title, location, units on scene/cleared, vehicles, tags) and above the call spine - incident list: above the timeline groups - Archive (/calls): above the transcript rows - node detail: above the Recent Calls table - Watch//alerts: above the events table, whose Snippet column is transcript text and whose keyword match was made against it - Live map: the desktop incident rail, pinned above the scroll area so it cannot be scrolled off the screen it qualifies; the mobile drawer; the incident marker popup; the incident-path stop popup - /systems: the source-call transcript preview - /features: the two marketing sections that describe the AI pipeline A1 — components/ui/UnbuiltMarker.tsx marks a claim unbuilt inline: - /faq: the retention answer promised 7/90/365-day windows. There is no TTL and no deletion sweep anywhere in the product (server-26#44), so the answer now states plainly that nothing is deleted automatically and marks per-plan retention as not yet available. - /settings/billing: the plan cards' claims — custom retention, SSO/SAML, uptime SLA, data residency — are marked not-yet-available next to the plan that makes them. Labelling only. No retention, SSO, SLA or residency was built; no billing, Stripe or checkout code was touched (Gate B still bars charging anyone); no price was added anywhere; no Python was touched. Both themes verified against the light-mode !important overrides in globals.css, which are untouched. tsc --noEmit clean. Refs: server-26#46, server-26#44, server-26#48 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
be79499635 |
Give the nav's dead links somewhere to land
Three of the app's routes were referenced but never existed, so the redesign's
navigation pointed at 404s from several directions.
/dashboard was the post-login and fallback redirect target in nine places --
login, onboarding, middleware, the admin/nodes/systems/tokens/settings guards,
and the marketing header -- but app/dashboard/ was never created. Signing in
normally dropped the user on a 404. The real signed-in home is "/", which
app/page.tsx already renders as LiveView for an authed user with an org, and
which the nav labels "Live"; all nine now point there.
Nav also linked /watch and /network, neither of which existed. /watch is the
alerts screen under its redesign name, so it re-exports app/alerts/page.tsx
and /alerts stays reachable for old links. /network is new: the "my equipment"
hub the redesign moved /nodes, /systems and /tokens behind and then never
built, which had left /systems and /tokens with no entry point in the UI at
all. Its hooks all run before the admin/operator guard, per
|
||
|
|
83416fe169 |
Split platform-admin from org-owner, hide Trips from non-founding orgs
SAAS_PLAN.md B7. "admin" meant two different things before this: platform operator (SAAS_PLAN.md's own framing) and, by accident of how app/settings/layout.tsx was gated, the only role that could ever reach an org's own billing/members/node-ownership settings. A paying customer who is their own org's owner couldn't reach their own Settings page - the gate checked isAdmin, which only platform admins ever have. settings/layout.tsx now admits org_role === "owner" as well as platform admins (isAdmin stays valid too, for support access to any org's settings). Nav.tsx shows the Settings link on the same condition, and moves Admin (the platform-operator screens: feature flags, users, audit, correlation debug) out of the customer-facing link group entirely - it was already gated server-side, this is just the nav no longer implying it's part of the product. Trips - an internal utility feature riding along on this stack, not a tenant-scoped product surface (see [[trips-feature-intentional]]) - drops out of the customer-facing viewer link group and only shows for the founding org (new lib/tenancy.ts mirrors app/internal/tenancy.py's FOUNDING_ORG_ID) or a platform admin, matching the mutation-route gating routers/trips.py already got in the backend tenancy commit. Reads stay open to any signed-in user, same as before - trips' own visibility model (public/private per trip) predates and is unrelated to org tenancy, and restricting it further wasn't asked for. Also closes two DEFERRED.md items now that they have somewhere to write to: app/settings/organization's "Save changes" button now actually calls c2api.getOrg()/updateOrg() (routers/org.py, shipped in the backend tenancy commit) instead of being permanently disabled. app/settings/nodes gained an EnrollmentTokensPanel (mint/list/revoke against the same commit's /org/enrollment-tokens routes) - without this, B2b's whole point (a customer enrolls their own node with their own token instead of an admin-issued key) had no way to actually be used outside a raw API call. Left alone, and written up as new DEFERRED.md entries instead of guessed at: node/system *write* routes (approve, create, delete) stay platform-admin-only rather than being loosened to org owner/operator - a real gap per SAAS_PLAN.md 2.4, but a separate authorization design that the plan's 12-item build order doesn't enumerate. And settings/members + settings/nodes' ownership table both still call GET /admin/users (platform-admin-only) - a pure org owner who reaches the page via this commit's gate will get 403s from it. Today's only real user is also a platform admin, so this is invisible until a second, non-admin org owner exists. Typecheck: clean (tsc --noEmit via the WSL-native ~/drb-frontend copy). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
53965e1a19 |
Rebuild the frontend as a product rather than an internal tool
The UI worked but read as an operator console: no public face, no way to describe or sell the thing, and no account surface beyond the node list. This adds the missing halves and reorganises what was already there around the incident, which is the unit of value the rest of the pipeline is built to produce. A shared design system replaces per-page styling: components/ui (Button, Card, Badge, EmptyState, Skeleton, PageHeader), a type scale and shadow set in the Tailwind config, and light-mode tokens in globals.css. The existing html:not(.dark) remap mechanism is extended rather than replaced -- a parallel theming system would have been two sources of truth for the same colours. Public marketing pages (/, /features, /pricing, /faq) load without a session. middleware.ts gained a PUBLIC_PATHS allowlist to permit that; it remains a UX redirect and is still NOT an authorisation boundary, which the comment there says explicitly. Real enforcement is unchanged and still lives server-side in c2-core's auth.py. Chrome switching is done by pathname in ChromeSwitcher instead of by route group, because a route group would have collided on / and forced most of app/ to move for no behavioural gain. Billing and API keys ship as typed stubs, not integrations. lib/billing.ts and lib/apiKeys.ts define the data model and the screens consume it, but every mutating call throws with a message naming the backend route that has to exist first, and the sample data is labelled as sample. Nothing here can charge anyone or mint a real credential -- picking a payment processor and holding its keys is a decision for a human, and a half-wired checkout is worse than an obviously absent one. The severity work from the c2-core change lands here too. severity is now a filter and sort dimension on the incident list rather than decoration, since a busy dispatch channel is only readable if you can collapse it to moderate and above. routine gets a muted treatment because it is the majority of traffic, legacy "unknown" still renders nothing, and TypeBadge handles the new "other" incident type. Severity rendering moved into lib/severity.tsx so the incident list, incident detail and call rows cannot drift apart. Deliberately not touched: calls, map, alerts, nodes, systems, tokens, trips and admin. They already share the palette and stay coherent, and rewriting them would have buried the parts that actually needed to change. No colour tokens were renamed, so nothing regressed there. Verified with tsc --noEmit (npm run typecheck), clean. No runtime verification was possible and none was done. No new environment variables. |