Phase10 followups and clients#1
Merged
Merged
Conversation
…tem) TECH_DOC.md S9 Phase 10 -- request_count/avg-duration/bytes/requests-per-sec aggregation over a trailing window, both per-project and per-team. server/migrations/0001_init.sql: request_logs gains duration_ms (connection lifetime -- this server forwards bytes opaquely and never sees an actual HTTP response, S2, so "response time" here means stream-open to stream-close/session-eviction, not true latency -- same honest gap already documented for status_code) plus an index on (project_id, timestamp), since the new /stats endpoints filter+aggregate by project over a time window. Edited in place rather than an additive migration, per this project's established pre-beta convention (nothing deployed yet). duration_ms captured at every logging::log_queue call site: https_listener.rs and tcp_router.rs via an Instant captured at connection start; udp_sessions.rs's ForwardEntry gained created_at so the reaper's EvictedSession carries a real session lifetime instead of a placeholder. New endpoints (server/src/api/mod.rs holds the shared TrafficStats/ StatsQuery types, window clamped to [60s, 30 days]): - GET /api/projects/:id/stats?window_secs= - GET /api/teams/:id/stats?window_secs= -- team totals plus a per_project breakdown array Real bug caught by the new team-stats test, not just plumbing: ProjectTrafficStats' nested `stats: TrafficStats` field was missing #[serde(flatten)], so the JSON response nested every stat under a `stats` key instead of flattening it alongside project_id/project_name -- the test asserting on `per_project[i]["request_count"]` failed against a real `null` until this was added. Also fixes a stale doc comment on get_project_logs claiming nothing writes to request_logs yet -- false since logging::log_queue was wired in. 6 new tests (project stats: aggregates within window / empty-state; team stats: totals + per-project breakdown / non-member forbidden). 126 total passing (was 122). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
raba.service.template's AmbientCapabilities comment only mentioned RABA_HTTPS_PORT/:443 -- stale since Phase 10 added RABA_HTTP_PORT (default 8080, e.g. :80 for the plain-HTTP redirect listener), which needs the same capability if bound below 1024. Functionally harmless (AmbientCapabilities already covered any privileged port), just misleading to read. dynamic-domains-tls.md: the existing SNI-passthrough templates (nginx stream/Caddy layer4) only work for :443 because they forward opaque encrypted bytes with no need to understand the payload -- :80 plaintext HTTP has no SNI to route by, so a shared box already running another webserver on :80 has no equivalent passthrough option. Recorded as a known, deliberately unaddressed gap rather than silently missing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds TrafficStats/ProjectTrafficStats/TeamTrafficStats to api/client.ts (projectsApi.stats, teamsApi.stats) matching the server's GET /api/projects/:id/stats and GET /api/teams/:id/stats shapes. ProjectDetail gets a "Traffic (last hour)" stat-card row (requests, req/sec, avg duration, bytes in/out); TeamSettings gets the same totals plus a per-project breakdown list. Both reuse the existing StatCard component rather than introducing a new one. avg_duration_ms is documented at the type level as connection lifetime, not true HTTP response latency (the server forwards bytes opaquely and never sees an actual response) -- same caveat carried from the server's logging::log_queue doc comment, so the dashboard doesn't imply a precision the data doesn't have. Extracted formatMs/formatBytes into a shared src/lib/format.ts rather than duplicating them across ProjectDetail and TeamSettings. tsc --noEmit and `npm run build` both clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
client/raba-core/src/management_api.rs was built against TECH_DOC.md's documented endpoint list before Phase 7's server routes existed (its own doc comment flagged this, and TODO.md tracked it as a known, blocking gap since 2026-07-10). Now that the server is real: - create_project was missing the now-required team_id and (for HTTP) subdomain -- every HTTP project creation from the CLI would 400 against a real server. Fixed, plus ProjectInfo/CreatedProject gained the fields the server actually returns (team_id, domain, local_target, online). - No team/domain/stats support existed at all. Added list_teams, update_team_domain/verify_team_domain, get_project_stats/ get_team_stats -- new TeamInfo/DomainInfo/TrafficStats/ ProjectTrafficStats/TeamTrafficStats types matching the server's response shapes exactly. client/raba-cli/src/cli.rs: - New resolve_team() helper: an explicit --team (id or name, case- insensitive) if given, otherwise the caller's sole team if they belong to exactly one, otherwise an error listing the options rather than guessing. - New default_subdomain(): auto-generates a subdomain from the project name when --subdomain is omitted (lowercased, non-alphanumeric runs collapsed to '-', trimmed) -- previously nothing generated one at all, a real functional gap since the server requires subdomain for HTTP projects (server/src/api/project_routes.rs). Avoids the server-reserved "tunnel" word as an exact match. - http/tcp/udp/connect gained --team (all three) and --subdomain (http only) flags; resolved choices persist into .raba.toml's new team_id/subdomain fields so a later run in the same directory doesn't need to re-decide. - A 409 from create_project (projects_domain_subdomain_unique -- see 0001_init.sql) now gets a specific "subdomain already taken, retry with --subdomain" message instead of a generic wrapped error. - New `raba team list` / `raba team domain-set <domain>` / `raba team domain-verify` subcommands (team custom domains, docs/plan/dynamic-domains-tls.md) and `raba stats [--team|--project]` for the new traffic-stats endpoints. spawn_mock_server (management_api.rs test helper) changed from &'static str to impl Into<String> so per-test response bodies can be built with format! instead of leaking memory to satisfy 'static. 23 raba-core + 9 raba-cli tests passing (added: 6 management_api endpoint tests, 5 default_subdomain tests). TODO.md's Phase 10 table and the standalone "CLI/server API reconciliation" row both updated to [x]. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The dashboard was still on Vite's default scaffold branding -- a
hardcoded blue accent (ui.tsx's own comment: "Accent is blue") with no
relation to TECH_DOC.md's brand spec, and a blue "R" square standing in
for a logo in three separate places (sidebar, Login, Signup) because
the real mark didn't exist yet.
dashboard/src/index.css: new --color-accent-{50,400,500,600,700}
tokens via Tailwind v4's @theme, aliased to the brand green (#22C55E,
same hex as Tailwind's stock green-500). Named semantically ("accent"),
not by hue, so a future rebrand is a one-line edit here instead of a
grep-and-replace across every consumer. Every hardcoded blue-* utility
across ui.tsx, Layout.tsx, and 6 pages swapped to accent-* at the same
shade number.
Real bug caught while wiring this in: @theme blocks only accept custom
properties, no comments -- Tailwind's parser silently miscompiled the
CSS when a comment (with an apostrophe, which the minifier also chokes
on outside @theme) lived inside the block. Moved the explanation above
the block instead, both times fixed before it shipped, not after.
dashboard/src/components/Logo.tsx (new): LogoMark + Logo, the real
arch/badge mark inlined as SVG (crisp at small sizes, no extra
request), replacing the placeholder "R" square in all three spots.
dashboard/public/favicon.svg swapped from the default Vite React logo
to the real mark.
npx tsc --noEmit and npm run build both clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TECH_DOC.md §12 already fully specced the logo (arch bridging two
connection points, dark badge, green arch, orange connection-point
circles) -- built the actual SVGs from that spec rather than inventing
a new concept: docs-site/static/img/brand/{mark,wordmark,
lockup-compact,lockup-full}.svg, plus favicon.svg/.ico derived from
the mark via ImageMagick (4-size .ico: 16/32/48/64).
Two real bugs caught before shipping, not after: text used
fill="currentColor", which only resolves inside an inlined SVG with a
CSS color context -- referenced as a plain image file (README, <img>,
favicons) it rendered invisible, fixed to a fixed dark fill instead.
Separately, the explanatory comments used "--" as a separator (this
codebase's usual comment style), which XML forbids anywhere inside a
comment, not just as the closing delimiter -- broke well-formedness,
caught by parsing every file with expat before considering them done.
BRAND.md (new): the user/contributor-facing version of TECH_DOC.md
§12's spec -- name meaning, mark concept, colors, asset list. Kept
deliberately separate from TECH_DOC.md/TODO.md, which track internal
build progress and may not survive to v1 in their current form.
README.md (new, was an empty placeholder): logo, name meaning, feature
list, npm/Docker/CLI quickstarts, project status, license -- makes no
reference to TECH_DOC.md or TODO.md, both treated as internal-only per
this round's discussion.
LICENSE (new): Functional Source License 1.1, Apache-2.0 future
license (FSL-1.1-ALv2) -- verbatim from the official
getsentry/fsl.software template, not reconstructed from memory.
Permits any use, including commercial/internal-business self-hosting,
except offering the software as a competing hosted/managed service;
each version automatically converts to Apache-2.0 two years after its
release. Chosen over plain MIT (no restriction at all) and SSPL
(considered too aggressive/non-OSI) for this project's specific goal:
freely usable, but not resellable as a competing SaaS.
TODO.md: Build Phases summary table brought in sync with the actual
per-phase tables (Phase 9 still not started, Phase 10 now complete) --
was already correct in the detailed tables but stale in the summary.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TECH_DOC.md §9 Phase 9. Four independent parallel jobs (server, client, dashboard, docs-site) -- website/ deliberately excluded, already marked superseded in TODO.md (merging into docs-site, not built out further). Every job runs exactly the commands already verified by hand this session, nothing new invented for CI specifically. Prep fixes, made first so the gate isn't red on day one: - server/src/auth/jwt.rs: fixed 3 pre-existing clippy warnings (doc comment continuation-line indentation) -- server is now fully clippy-clean, so its job can enforce -D warnings. - client/raba-core/src/config.rs + tunnel_conn.rs: fixed the same for client (field_reassign_with_default in a test, and a documented #[allow(clippy::await_holding_lock)] for a std Mutex guard deliberately held across await in a test-only env-var-serializing helper -- explained inline why it's safe: shared with sync #[test] fns elsewhere so it can't become an async-aware Mutex, and every #[tokio::test] in this crate uses the default current-thread runtime flavor, so it can't deadlock against another thread). Real gap found and fixed while verifying docs-site's checks actually run clean: docs-site/node_modules was ~32KB with no package-lock.json at all -- scaffolded but npm install was never actually completed there. Ran it for real (1304 packages); typecheck and build both now pass and the lockfile is committed (ci.yml's cache-dependency-path and `npm ci` both need it to exist). rustfmt is deliberately NOT enforced yet -- the codebase isn't rustfmt-clean (894 lines would change in client/, 3773 in server/), and normalizing that is its own separate, deliberate change, not bundled into standing up CI. YAML verified structurally valid via js-yaml (no local Actions runner available to fully exercise it) -- real end-to-end verification happens on the first real PR/push. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The server API for team custom domains (PATCH .../domain, POST .../verify-domain, docs/plan/dynamic-domains-tls.md) has existed since Phase 7/10, and raba-cli got team domain-set/domain-verify in the client reconciliation pass -- but the dashboard itself never got a form for it. TeamSettings.tsx had zero domain UI at all. server/src/api/team_routes.rs: adds GET /api/teams/:id/domain -- a real gap, not just a client-side omission. PATCH and verify-domain both return the current DomainResponse as the *result of an action*, but there was no way to plain-read the current state, so the dashboard couldn't show existing config on page load without provoking a domain change or a re-verify as a side effect. TeamRole::Member (view-only), same tier as get_team, not the Admin tier the two mutating endpoints require. 2 new tests. dashboard/src/api/client.ts: DomainResponse/CertStatus types plus teamsApi.getDomain/setDomain/verifyDomain. dashboard/src/pages/TeamSettings.tsx: new DomainSection -- shows the current domain (if any) with verified/cert-status badges and the DNS records to add, a set/change form and a verify button (both gated canManage, matching every other mutating action on this page), and a plain read-only view for non-admins. getDomain's 404 (no domain set yet) is handled as an expected empty state, not an error -- fetched separately from the page's other Promise.all data so it can't reject the whole load and hide the rest of the page. dashboard/src/components/Logo.tsx: arch color now reuses --color-accent-500 instead of duplicating its hex -- real DRY, since that token already exists dashboard-wide and the two values could otherwise silently drift apart. Badge background and connection-point orange stay plain hex (TECH_DOC.md §12's fixed brand colors, used nowhere else in the dashboard, so a token would only add indirection without preventing any actual duplication). tsc --noEmit, npm run build, npm run lint all clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…team
Previously POST /api/projects required team_id unconditionally, and
raba-cli errored out ("multiple teams found, pick one") the moment an
account had more than one team and no --team was given -- annoying for
what's supposed to be a zero-config CLI, and not what was actually
wanted: a silent, sensible default (the caller's oldest team
membership, in practice almost always their signup-created personal
team) rather than forcing every invocation to specify one.
server/src/api/project_routes.rs: CreateProjectRequest.team_id becomes
Option<String>. New resolve_default_team_id -- no schema change
needed, team_members.created_at already exists -- ORDER BY created_at
ASC LIMIT 1 for the caller. This is deliberately the *only* place the
default lives (not duplicated client-side) so it can never drift from
what the server actually does. 4 new tests: sole-team case (regression
check), multi-team defaults to the oldest (seeded with an explicit
created_at via direct SQL, not wall-clock timing -- SQLite's
CURRENT_TIMESTAMP only has second resolution, so two teams created
back-to-back through the API within the same test could tie and make
"oldest" nondeterministic), explicit --team still overrides the
default even with multiple teams available, and the defensive
zero-teams case (can't happen through normal signup, exercised via
direct team_members deletion).
client/raba-core/src/management_api.rs: create_project's team_id
becomes Option<&str>, omitted (not sent as null) from the JSON body
via #[serde(skip_serializing_if)] when absent -- the server
distinguishes "omitted, please default it" from an explicit null,
which serde wouldn't accept for a String field anyway. spawn_mock_server
(test helper) now captures the full request (was request-line only) so
tests can assert on JSON body content, not just method/path -- needed
to actually prove team_id is omitted, not just null. 2 tests (send
it when given, omit it entirely when not).
client/raba-cli/src/cli.rs: resolve_secret's Create branch only calls
resolve_team (local validation against the caller's own teams, nicer
error than a raw 403) when --team was actually given; when omitted, no
client-side resolution/guessing at all, team_id just isn't sent and
the server decides. resolve_team itself is unchanged and still used
as-is by `raba team`/`raba stats`, which always need a concrete
team_id for their URL path -- a different situation from project
creation, where omission is now a legitimate choice.
132 server tests, 33 client tests (24 raba-core + 9 raba-cli), all
passing. cargo clippy --all-targets -D warnings clean across server
and client.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ller's teams Adds the third way to pick which team a project belongs to (alongside the existing team_id and the just-added default-to-oldest-team): an explicit domain, resolved server-side to whichever team owns it. server/src/api/project_routes.rs: CreateProjectRequest gains an optional `domain` field. - Equal to the instance's own domain -> treated identically to omitting it entirely (not a team domain, nothing to resolve). - Otherwise resolved via resolve_team_id_from_domain, scoped to teams the caller can actually act on -- never a global lookup, or any authenticated caller could probe which team owns an arbitrary domain string just by trying to create a project under it. Instance admins/owners search every team's domain instead (consistent with require_team_role's existing is_admin_or_owner() bypass elsewhere in this file -- they can already act on any team's projects regardless of membership, so scoping --domain differently for them would just be a confusing gap, not an extra safety boundary). - Matched but unverified -> explicit 400, not a silent fallback to the instance domain -- the caller asked for this domain specifically, so silence would be worse here than in the implicit (no domain given) path, which does still fall back silently (existing behavior, unchanged). - domain given alongside an explicit team_id that disagrees with the resolved owner -> 400. 9 new tests: resolves to the owning team (single team and picked correctly among several), rejected when no team the caller can act on owns it, rejected (not silently ignored) when matched but unverified, team_id+domain agreeing/disagreeing, instance-domain-equals-omitted, and the two security-relevant ones -- scoped away from an unrelated account, but global for instance admins. client/raba-core/src/management_api.rs: create_project gains a `domain: Option<&str>` parameter, sent (or omitted) the same way team_id already is. 1 new test. client/raba-cli/src/cli.rs: new --domain flag on http/tcp/udp/connect, persisted into .raba.toml the same way --team/--subdomain already are. Passed straight through to create_project unresolved -- the server decides which team it belongs to, not this module. 150 server tests, 34 client tests (25 raba-core + 9 raba-cli), all passing. cargo clippy --all-targets -D warnings clean across server and client. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… domain The implicit domain-resolution path (no explicit `domain` in the create request) has always silently used the instance domain when the resolved team's own domain isn't verified yet -- correct behavior, but silent, leaving the caller to wonder why their project didn't land on their team's domain. The explicit-domain path already 400s outright on the same situation (added earlier this session); this is the implicit path's counterpart, minus the hard failure -- it can't error, since nothing was explicitly requested to fail on. CreatedProjectResponse gains an optional `warning` field, set only when: the team resolved (via team_id or the default-team fallback, never via an explicit `domain` -- that path already rejects the unverified case outright) has a domain row that exists but isn't verified. The domain-resolution query no longer filters `WHERE verified = 1` -- it needed the unverified row's value too, to name it in the warning, not just know a verified one didn't exist. 2 new tests: the warning fires with the right domain name when the team's domain is set-but-unverified, and stays absent when the team has no domain configured at all (only the more specific "set but unverified" case should ever warn). 153 server tests passing (was 150). clippy --all-targets -D warnings clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
default_subdomain(name) slugified the *project* name into a subdomain when --subdomain was omitted. The project name commonly falls back to the current directory's basename (config::resolve_project_name) -- for a tunneling CLI invoked from wherever, that's often a completely unrelated folder name (downloads, scratch, whatever you happened to be cd'd into) with no business ending up in a public subdomain. Replaced with random_subdomain(): a fun, human-readable adverb-adjective-noun-NN name (e.g. briskly-brave-otter-42), Docker/Heroku-style, matching how ngrok/localtunnel actually behave by default (random unless you ask for something specific via --subdomain). No stability lost -- once created, the subdomain is already persisted into .raba.toml and the secret cached, so a later `raba connect` in the same directory reuses the exact same one regardless of how it was first picked; the randomness only affects the very first run. No new dependency: a nanosecond timestamp mixed with the process id is plenty of entropy for a friendly, non-security-sensitive name (unlike auth::secret's actual project secrets, which do need a real CSPRNG). SmallRng (splitmix64's finalizer, hand-rolled, no crate) decorrelates the four picks drawn from that one seed -- naively shifting/masking the raw seed per pick would make adjacent choices visibly correlated. 30 adverbs x 34 adjectives x 30 nouns x 100 suffixes = over 3 million combinations. 3 new tests (shape, DNS-safe charset, variation across 50 calls) replacing the 4 old default_subdomain-specific ones. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes out the client/dashboard reconciliation gap left over from Phase 10, adds CI, and lands branding/licensing — all follow-ups identified while wrapping up hardening on
feat/dynamic-domains-tls.raba-core/raba-clihad drifted from the real server API (missingteam_id/subdomainon project creation, no team/domain/stats support at all). Brought current:list_teams,update_team_domain/verify_team_domain,get_project_stats/get_team_stats, newraba team/raba statssubcommands.ProjectDetail/TeamSettingsnow show live traffic stats;TeamSettingsgained the custom-domain view/set/verify form it never had (plus theGET /api/teams/:id/domainendpoint that was missing server-side to support it).team_idis now optional (defaults to the caller's oldest team membership instead of erroring on ambiguity), and a new explicit--domainflag resolves whichever team owns it — scoped to the caller's own teams, global for instance admins. Silent unverified-domain fallback now surfaces awarningin the response instead of failing silently.adverb-adjective-noun-NNgenerator, no new dependency.ci.yml— test + clippy (-D warnings) across server and client, typecheck + build + lint for dashboard and docs-site, 4 parallel jobs. Server and client both brought to a clean clippy slate first. Found and fixed a real gap:docs-site/had nopackage-lock.jsonand incompletenode_modules—npm installhad never actually been run there.TECH_DOC.md's existing brand spec, dashboard re-themed from placeholder blue to the brand green (via a proper CSS custom-property token, not hardcoded utility classes), a realREADME.md,BRAND.md, andLICENSE(Functional Source License 1.1 — free for any use including commercial self-hosting, except reselling as a competing hosted service; converts to Apache-2.0 after 2 years).docker/Dockerfile'sRABA_CERTS_DIRwasn't pointed at the persistent volume (certs would've been lost on container recreate);.env.examplebrought current; systemd template comment and:80shared-VPS gap documented.Test plan
cargo test+cargo clippy --all-targets -D warningsclean acrossserverandclient(153 server tests, 34 client tests)npx tsc --noEmit,npm run build,npm run lintclean fordashboardnpm run devanddocker compose upboth confirmed working end-to-endci.ymlitself has not yet run for real on GitHub (no local Actions runner available) — first real signal will be this PR's own checksNotes for reviewers
CreateProjectRequest.team_idchanged from required to optional — API-compatible (existing callers passing it explicitly are unaffected), but worth flagging as a contract change.--domainresolution is scoped to the caller's own teams for regular users, and instance-wide for admins/owners — deliberate, matchesrequire_team_role's existing admin bypass elsewhere in this file; flagging in case that scope decision needs a second look.