Repository navigation
fix: make three console surfaces state what the deployment actually does - #1336
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reachedNext included review available in 39 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (26)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Visual proofCaptured against the demo box on 2026-08-29, with this branch built into its own image (hive-web-console-prod:proof1336) and run beside the live stack on the same compose network, reading live control-plane data as demo@hive-demo.invalid. The sign-up gate here comes from the box own ENTERPRISE_DISABLE_SIGNUP=true, not from a value chosen for the capture. 1: the balance reads $99.99 with the credit figure and the conversion beneath, where it used to read 99,996,364,207 with no unit (#1332). 2: the active key row offers Revoke only, zero anchors point at a rotate href, and the header names the path that works (#1331). 3: the route this deployment refuses says so and offers sign-in, instead of the AUTH-reference server error (#1328). 4: sign-in no longer links to that page at all (#1328). Full capture log: docs/proof/console-honest-surfaces-2026-08-29/capture-log.md |
Adversarial reviewStreams
Findings and dispositions
Mutation checks. Every new assertion was run against a deliberately broken copy of the code it covers, and every group went red: the flag forced open, the refusal classifier disabled, the balance headline reverted to a bare integer, the rotate link restored, and the dollar-space truncation restored. Fifteen failures across six suites, then green again on revert. |
Issue #1328, sign-up. Every deployment this repo ships refuses POST /auth/v1/signup twice over: deploy/docker/Caddyfile.supabase answers 404 for that path on the public listener, and GoTrue runs with signup disabled. The console shipped and linked a sign-up form anyway, and the bare 404 arrived at the browser as an auth-js AuthUnknownError with no status, which the allow-list in lib/auth/auth-error.ts correctly withheld, so a stated policy reached the visitor as "Something went wrong on our end. Reference AUTH-...". The policy is untouched. The route is now gated on a deployment flag read from the same variable that drives the GoTrue flag, in the same polarity, so re-enabling signup stays one value in one place. Where self-serve is off, the page says accounts are created by invitation and points at sign-in, the sign-in cross-link drops rather than promising a page that cannot complete, and a refusal that does reach the endpoint is reported as a refusal. Issue #1331, rotate. Every active key linked to a rotate route that does not exist, so the action answered "This page could not be found". The link and the promise in the page header are gone, replaced by the path that does work (create a replacement, revoke the old). The unused rotateApiKey client helper is deleted with it: it decoded the wrong response shape and would have thrown for any caller that ever used it. Issue #1332, denomination. The dashboard printed the balance as a bare grouped integer while the API keys table printed the same quantity in dollars. One shared component now draws the balance in dollars on both the dashboard and the billing page, with the credit figure and the conversion under it, and balances truncate rather than round so a balance never reads as more money than it is.
8,290,000,000 credits is 8.29 dollars, and 8.29 times 100 is 828.9999999999999 in IEEE 754, so truncating the dollar product printed $8.28 and understated a real balance by a cent. CREDITS_PER_USD is a power of ten and the digit count never exceeds nine, so the same truncation in credit space is exact for every integer balance. Found in self review of this branch.
… auth error shapes Two findings from the adversarial review stream. A non-finite credit value can only come from a decode that failed, and printing it as $0.00 asserted an empty wallet where nothing was read; it now renders the same em dash formatPercent uses for the same reason. The refusal classifier keeps its AuthUnknownError rule, with the measurement that justifies it recorded next to it: auth-js maps 500, 502, 503, 504 and a dead transport to AuthRetryableFetchError, so no outage shape it recognises can reach that branch. The test now uses those real shapes rather than an invented one.
661febe to
ba3bc21
Compare
Visual proofRecaptured at ba3bc21, the reviewed head, so this supersedes the earlier comment which predates the credit-space truncation fix and the em dash for an unreadable balance. Same substrate: this branch built into hive-web-console-prod:proof1336 and run beside the live stack on the demo box compose network, reading live control-plane data as demo@hive-demo.invalid, with the sign-up gate coming from the box own ENTERPRISE_DISABLE_SIGNUP=true. 1 and 2: the same balance on the dashboard and on billing, drawn by one shared component, $99.99 with the credit figure and the conversion beneath (#1332). 3: Revoke only on the active key row, zero anchors at a rotate href, and a header that names the path that works (#1331). 4: the refused route says so and offers sign-in, instead of the AUTH-reference server error (#1328). 5: sign-in no longer links to it at all (#1328). Capture log: docs/proof/console-honest-surfaces-2026-08-29/capture-log.md |
|
@coderabbitai full review |
|
|
@coderabbitai full review |
|
Review status, finalStreams
Two more fixes since that comment, both from my own re-read of the final diff
Threads. There are no inline review comments on this PR ( |
|
Tip For best results, initiate chat on the files or code changes.
The No CodeRabbit inline review thread requires resolution. 🐇 ✏️ Learnings added
You are interacting with an AI system. |
Closes #1344 Follow-up to #1336, which merged while this last fix was still in flight. `available_credits` is posted minus reserved, so a workspace whose holds exceed its posted credits reads negative. `formatUsdBalanceFromCredits` rounded with `Math.trunc`, which rounds toward zero, so an overdrawn balance displayed less of the hole than was actually there. `Math.floor` is identical for every positive balance, which is every balance the demo box has today, and conservative for a negative one. The docstring said "truncated toward zero"; it now states the direction it actually rounds, so the comment cannot drift from the code again. ## Test One case, in `lib/format/format.test.ts`: `-8,295,000,000` credits renders `-$8.30`, not `-$8.29`. It fails on `Math.trunc` and passes on `Math.floor`, which was checked by reverting. `npx vitest run lib/format/format.test.ts tests/unit/credit-balance.test.tsx` in Docker with `--build` on a private image tag: 30 passed. No visual change on any surface that exists today, since every live balance is positive and `Math.floor` and `Math.trunc` agree there. The proof captures on #1336 stay accurate. ## Buglog entry ```json {"date":"2026-08-29","area":"web-console","error_message":"A negative credit balance displayed less debt than the account carried","root_cause":"formatUsdBalanceFromCredits rounded with Math.trunc, which rounds toward zero, so an available balance driven negative by reservations was flattered by one displayed unit","fix":"Round with Math.floor, identical for positive balances and conservative for negative ones, and state the direction in the docstring","tags":["web-console","billing","credits","issue-1332"]} ```
…#1364) ## What this changes Repo-local agent tooling only. No product code, no workflows, no runtime behavior. Linked to no issue. ### 1. Four dead skills deleted `.claude/skills/debug-issue.md`, `explore-codebase.md`, `refactor-safely.md` and `review-changes.md` all instruct the agent to call a `code-review-graph` MCP server: `get_minimal_context`, `semantic_search_nodes`, `query_graph`, `get_flow`, `list_flows`, `detect_changes`, `get_impact_radius`, `get_affected_flows`, `get_architecture_overview`, `list_communities`, `find_large_functions`, `refactor_tool`, `apply_refactor_tool`. That server does not exist here. Verified in this worktree before deleting: - No `.mcp.json` at the repository root, and `find . -name .mcp.json` returns nothing anywhere in the tree. - No `graphify-out/` directory in the checkout. - Outside those four files, the string `code-review-graph` appears only in `.gitignore` and a historical note in `.wolf/cerebrum.md`. - The tool names themselves (`get_minimal_context`, `semantic_search_nodes`, `query_graph`, `get_impact_radius`) appear in those four files and nowhere else. An agent invoking one of these is told to call tools that are absent, and the likely failure is improvisation rather than a clean error. Their shared "at most 5 tool calls, at most 800 output tokens" budget is meaningless without the graph it was written for. **`review-changes` is deleted rather than rewritten**, and that was a judgment call worth stating. Rewriting it around `git diff` and `gh pr diff` leaves a file whose remaining content is a pointer to `prove-test-load-bearing.md` and `pr-ci-status.md`, both of which already carry front matter descriptions that route agents to themselves, plus a restatement of the review sequence that `.claude/rules/orchestrator.md` stage 6 already owns and that the global `adversarial-pr-review` skill defines in full. A third copy of that sequence is a third thing to keep in sync, and the two skills it would point at are already discoverable. The two genuinely repo-specific lessons that a rewritten `review-changes` would have carried are folded into `prove-test-load-bearing.md` instead, where the surrounding material is about exactly that. ### 2. Patterns added **Assert on the wire, not on the struct** (`prove-test-load-bearing.md`). For anything crossing an HTTP or SSE boundary, assert on serialized bytes: `httptest.ResponseRecorder.Body`, the SSE frame text, or a decoded `map[string]any`. A struct assertion is blind to a missing `json:"..."` tag, a custom `MarshalJSON`, an `omitempty` erasing a legitimate zero, and any downstream rewrite. Names this repository's three body-rewriting layers, since a struct assertion upstream of any of them cannot see what actually goes out: - `injectMemoryBlock`, `apps/edge-api/internal/chat/memory.go` - the Anthropic request translator, `apps/edge-api/internal/anthropic/translate_request.go` - `buildMessagesFromInput`, `apps/edge-api/internal/inference/responses.go` Worked example is issue #1329 / PR #1334: `StreamUsage` carried `omitempty` on every field, so `message_start` serialized as `"usage":{}` with no required member at all. The struct was correct; the serialization was not. `apps/edge-api/internal/anthropic/usage_wire_test.go` is the guard that can see it, and its own header states the same lesson. **The gate's scope is not the gate's name** (`prove-test-load-bearing.md`). A gate can be green because it never examined the thing. Two shapes, both from real merges: - A scope narrower than its name. `scripts/test-owui-hive-frontend.sh:175` runs `node owui-hive-svelte-compile-check.mjs lib/hive`, so coverage stops at that one directory. Three simultaneous mutations including a hard parse error left the suite reporting `16 passed / 208 passed / 13/13 components compiled`, exit 0 (PR #1298). Every count was true; none of them counted the broken file. - A gate nothing ever runs. `.claude/hooks/hooks.selfcheck.js` had real cases and no caller, which is how a secrets scanner blind to every MultiEdit survived (issues #1333, #1339). PR #1337 wired it into `ci.yml:705`. The check to actually run: name the file you changed, read the gate's invocation rather than its title, find the path or glob argument, and confirm your file is inside it. Negative control offered: break the file on purpose and rerun the gate. **A CONFLICTING PR runs no CI at all while looking green** (`pr-ci-status.md`, new cause 0, ahead of the existing three because it invalidates the evidence they are diagnosed from). Read `mergeable` before `mergeStateStatus`. `mergeable: CONFLICTING` means GitHub cannot build `refs/pull/N/merge`, so no `pull_request` workflow run is created at all, so `statusCheckRollup` is empty or stale and the page reads as "no problems found" rather than "not evaluated". Observed on PR #1336: while CONFLICTING it showed only CodeQL, GitGuardian and CodeRabbit, which scans as green at a glance; the full suite ran only after a rebase. Notes `.wolf/buglog.jsonl` as the recurring cause on this repo, since GitHub's server-side merge ignores `merge=union` (issue #873). The waiting-for-CI section gains one line: an empty `statusCheckRollup` is ambiguous between "not started" and "cannot start", so read `mergeable` first. **`mergeStateStatus: UNKNOWN` means retry, not fail** (`pr-ci-status.md`, new section ahead of the BLOCKED material). UNKNOWN means GitHub has not finished computing the merge commit, and it appears right after a push, a base update, or a force-push. Poll again after a few seconds. Treating it as failure produced two false NOT-MERGED reports in one session, on PRs that had in fact merged. ### 3. Two corrections - `memory-tools.md` listed claude-mem as a live option with an `mcp__plugin_claude-mem_mcp-search__*` detection hint and a `mem-search` instruction. claude-mem was retired 2026-06-12 and survives only as a read-only sqlite archive at `~/.claude-mem/claude-mem.db`. The row now says so and tells the agent not to look for those tools. `memory-layers.md` line 71 carried the same stale `mem-search` pointer and is corrected the same way. - `worktree-compose-stack.md` gains one paragraph: `scripts/set-compose-project-name.sh:103-107` writes `deploy/docker/.env` unconditionally but the repository-root `.env` only if that file already exists, so the script has to be re-run with `--check` after an `.env` is created or copied, not only once at worktree creation. A worktree namespaced before its `.env` was copied in carries the namespace on one file and the default `hive` project name on the other, which is a container collision waiting to happen. ### 4. New skill: `owui-fork-edits.md` Nothing in `.claude/skills/` encoded the Open WebUI patch discipline, which is expensive to rediscover. Covers: - The chat image builds only the frontend from `vendor/open-webui` and replaces `/app/build`; the backend comes from a pinned upstream image, so a backend edit under the vendored tree is inert and produces no error. - A version assertion in the final stage fails the build when the vendored tree and the pinned backend disagree. - Backend changes go through `deploy/docker/owui-patches/`, and every patch asserts its own effect: an anchor that does not match exactly once prints what it expected and exits 1, and Python splices are `ast.parse`d before writing. - `Caddyfile.owui` is a third layer that can 404 a route independently of both others. - `dump_bundle_excerpts.py` plus `pinned-bundle-excerpts.json` is how the literal bundle assertions stay honest, since PR CI never builds the image. It also retires one stale claim that several patch comments still assert in their justifying prose: "every tenant OWNER holds the Open WebUI admin role" is no longer true. `owui-patches/tenant_role_from_db.py` lines 17 to 30 revoked that mapping, and OWUI `admin` now requires an ACTIVE `owner` row in `public.account_memberships` on an account with `is_platform_admin = true`. A tenant OWNER is an ordinary OWUI `user`. The patches remain correct defence; only the prose is stale. ### 5. `CLAUDE.md` The paragraph enumerating project-level skills is replaced with a pointer to the directory. That list had gone stale in both directions: it named four skills that had been dead for months, and omitted six that existed (`memory-layers`, `pr-ci-status`, `pr-visual-proof`, `prove-test-load-bearing`, `verify-deploy-happened`, `worktree-compose-stack`). ## Verification - Absence of the MCP server confirmed by direct search in this worktree before deleting anything, as listed above. - Every factual claim added was read out of the file it cites: `scripts/set-compose-project-name.sh`, `deploy/docker/owui-patches/tenant_role_from_db.py`, `dump_bundle_excerpts.py`, `apply_credits_patch.py`, `deploy/docker/Dockerfile.open-webui`, `deploy/docker/Caddyfile.owui`, `apps/edge-api/internal/anthropic/types.go`, `usage_wire_test.go`, `scripts/owui-hive-svelte-compile-check.mjs`, `scripts/test-owui-hive-frontend.sh`, `.github/workflows/ci.yml`. - `node .claude/hooks/hooks.selfcheck.js`: 61/61 passed. - Every `.wolf/decisions.md` id cited (D-036, D-040, D-044, D-052) exists and is live; none of the revoked or retired entries are cited. ## Test plan - [ ] CI green. `.claude/*` is on the inert-path allowlist in `ci.yml`, so the required checks report green without their heavy steps; `CLAUDE.md` is not, so the full suite runs on that file. - [ ] `node .claude/hooks/hooks.selfcheck.js` still passes. - [ ] No product code, workflow, or runtime behavior touched. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WyEwUxZCArdn1ZUDkTvuQ1 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
## Summary This is the batched buglog follow-up for the pull requests merged to `main` on 2026-08-29. Its diff is `.wolf/buglog.jsonl` and nothing else. Per `.claude/rules/openwolf.md`, every fixed bug, error, failed test or failed build must be logged, but the line may never be appended on a fix branch. `merge=union` in `.gitattributes` resolves concurrent appends locally and is ignored by GitHub's server side merge, so two branches that both appended land in hard conflict there. An unmergeable pull request gets no `refs/pull/N/merge`, no `pull_request` run and therefore zero checks, and the required status gate then blocks the merge for a reason the page never states (issue #873). Each fix accordingly carried its entry in its own pull request body, and this pull request copies them onto `main` in one batch, which the protocol explicitly prefers over one pull request per entry. ## Scope examined Fifty nine pull requests merged to `main` on 2026-08-29. Forty eight of them carried at least one entry, for eighty two entries in total. Thirty two of those were already on `main` and are skipped, leaving fifty appended here from thirty four pull requests. The largest block of skips comes from #1342, the equivalent batch for the 2026-08-28 merges, which merged earlier the same day and already landed thirty six entries covering #1257, #1268, #1276, #1277, #1287, #1292, #1293, #1294, #1296, #1301, #1303, #1305, #1313, #1335 and #1337. ## What landed Fifty entries appended, one JSON object per line, append only. The 232 pre-existing lines are byte identical to `origin/main` (verified by hashing the first 232 lines of the result against the base file). Every line in the resulting file parses as JSON and carries `error_message`, `root_cause`, `fix` and `tags`. | Source | Entries | |---|---| | #1083 | 2 | | #1277 | 1 | | #1278 | 1 | | #1298 | 1 | | #1334 | 1 | | #1336 | 3 | | #1343 | 1 | | #1346 | 1 | | #1351 | 1 | | #1365 | 2 | | #1368 | 1 | | #1369 | 1 | | #1371 | 3 | | #1375 | 3 | | #1376 | 1 | | #1378 | 1 | | #1379 | 2 | | #1388 | 5 | | #1389 | 3 | | #1390 | 2 | | #1393 | 1 | | #1394 | 1 | | #1410 | 1 | | #1417 | 1 | | #1421 | 1 | | #1423 | 1 | | #1424 | 1 | | #1426 | 1 | | #1429 | 1 | | #1431 | 1 | | #1433 | 1 | | #1434 | 1 | | #1436 | 1 | | #1439 | 1 | Entries are copied verbatim from their source pull request bodies. Nothing was rewritten, no field was invented, and no field was added. No JSON needed repair: all eighty two extracted entries parsed on the first attempt and all four required fields were present on every one. ## Merged pull requests that carried no entry Eleven of the fifty nine. Recorded here because the gap is itself the useful signal. | Pull request | Title | Assessment | |---|---|---| | #1013 | chore(deps): bump the go-minor-patch group across 1 directory with 4 updates | Dependabot bump, no defect fixed, no entry expected | | #1015 | chore(deps): bump the go-minor-patch group across 1 directory with 6 updates | Dependabot bump, no entry expected | | #1016 | chore(deps): bump golang from 1.26-alpine to 1.27-alpine in /deploy/docker | Dependabot bump, no entry expected | | #1218 | chore(deps): bump postcss from 8.5.19 to 8.5.26 in /apps/desktop | Dependabot bump, no entry expected | | #1219 | chore(deps): bump golang.org/x/crypto from 0.41.0 to 0.52.0 in /apps/control-plane | Dependabot bump, no entry expected | | #1342 | chore: batch buglog entries for the 2026-08-28 merges | The previous batch pull request itself, correctly carries no entry of its own | | #1364 | chore: remove four dead skills and record the patterns that cost time | Protocol gap. The body records patterns that cost time, which is the shape of a buglog entry, but none was written as one | | #1383 | test: retire stale expected-failure markers, restore the ones that are true (#1381, #1382, #1324) | Protocol gap. Stale `it.fails` markers reading as red is a real defect that was fixed here and should have carried an entry | | #1384 | docs: correct D-047, hive-auto reverted to variable pricing (D-059) | Decision ledger correction, arguably a documentation defect, no entry written | | #1387 | chore(deps): bump next from 15.5.23 to 16.3.3 in /apps/agent-console | Dependabot bump, no entry expected | | #1398 | docs: rescue the 2026-08-25 parity captures and add the 2026-08-29 QA matrix evidence | Documentation and evidence rescue, no entry written | Six of the eleven are Dependabot bumps and one is the previous batch, so the genuine protocol gaps are #1364, #1383, #1384 and #1398. Of those, #1383 is the one worth a follow-up: it fixed a real defect class (a stale expected-failure marker reads as a red "Expect test to fail" and gets dismissed as pre-existing) and left no record. ## Entries skipped as already present Thirty two. Thirty of them matched an entry already on `main` on `error_message`, `id` or `fix`. Two more from #1278 are semantic duplicates that an exact match would have missed, and were skipped after reading the landed entries they duplicate: - #1278's `streaming content_block_start omits text field` entry is covered by the consolidated `bug-2026-08-28-anthropic-sdk-wire-conformance` entry landed from #1296, whose root cause names the same `omitempty` on `StreamContentBlock.Text`. - #1278's `GET /v1/models leaked an upstream provider name` entry is covered by `BUG-1284`, landed from #1300, which names the same `public.model_aliases.summary` publication path. #1278's third entry, on `top_k` forwarding producing a 400, is not covered anywhere on `main` and is appended here. #1342 recorded #1278 as fully "merged into #1296", which was accurate for two of its three entries. ## Note on entry quality One appended entry is thin: #1277's parity re-score record carries `error_message` of `n/a` and a root cause of "console had no privacy/data-policy surface at all". It is a parity gap record rather than a defect record. It is included exactly as written rather than embellished, per the protocol's preference for the author's own words. ## Test plan - [x] Branch cut fresh from `origin/main`, diff is `.wolf/buglog.jsonl` and nothing else - [x] First 232 lines byte identical to the base file (md5 match) - [x] All 282 resulting lines parse as JSON and carry `error_message`, `root_cause`, `fix` and `tags` - [x] No `.wolf/` telemetry (`anatomy.md`, `memory.md`, `token-ledger.json`, `hooks/_session.json`, `buglog.json`) in the commit - [ ] The six required checks report green via the inert path allowlist in `.github/workflows/ci.yml` --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>









Closes #1328
Closes #1331
Closes #1332
Three console-surface honesty defects from the quiet-window live walkthrough on 2026-08-29, all on the demo path, landed together because they are one class of problem: the console asserting something the deployment does not do.
#1328 A sign-up page this deployment refuses, reported as a server error
The refusal is deliberate and stays.
deploy/docker/Caddyfile.supabaseanswers 404 for/auth/v1/signupon the public listener, andhive-supabase-auth-1runs withGOTRUE_DISABLE_SIGNUP=true. Re-verified live while writing this:POST https://console-hive.scubed.co/auth/v1/signupreturnsHTTP/2 404withcontent-length: 0andvia: 1.1 Caddy.That empty body is the whole bug. auth-js parses the response as JSON, the parse throws, and the caller receives an
AuthUnknownErrorwhose message is a JSON parse error and whosestatusis undefined. The allow-list inlib/auth/auth-error.tswithheld it exactly as designed, so a stated policy reached the visitor as "Something went wrong on our end. Reference AUTH-...".What changed:
lib/auth/self-serve.tsreadsNEXT_PUBLIC_DISABLE_SELF_SERVE_SIGNUPand fails closed. Unset and empty both mean disabled, because an unset build arg reachesnext buildas an empty string.ENTERPRISE_DISABLE_SIGNUP, the same variable that drives the GoTrue flag, in the same polarity. One value in one place, so the console cannot advertise what the auth service refuses./auth/sign-uprenders an invitation notice and a link to sign-in when self-serve is off, and the form is absent rather than disabled. The sign-in cross-link drops in the same state.toUserFacingSignUpMessagereports a policy refusal as a refusal: a 404 or 403 from the auth origin, GoTrue own "Signups not allowed for this instance" copy, and the status-freeAuthUnknownErrorshape that the bare 404 actually produces. Everything else keeps the existing allow-list behaviour, including the generic message plus support reference for a real 500.Not changed, deliberately: the Caddy refusal and the GoTrue flag. Re-enabling self-serve signup is still the two-part change the Caddyfile comment describes, gated on the tenant provisioning path in
.wolf/decisions.mdD-023.Turnstile was ruled out before this work and is not involved:
NEXT_PUBLIC_TURNSTILE_SITE_KEYis empty on the console container andTURNSTILE_SECRET_KEYis unset on control-plane.#1331 Rotate is a dead link
/console/api-keys/[id]/rotatedoes not exist; the only child route is[id]/limits. The link and the word "rotate" in the page header are gone, and the header now names the path that does work: create a replacement, revoke the old one.The route was not built instead, on purpose. The control-plane endpoint exists (
POST /api/v1/accounts/current/api-keys/{id}/rotate), but shipping a new credential-issuing surface with its own secret-reveal step belongs in its own change with its own review, not bundled into a three-defect honesty PR. Worth knowing for whoever picks that up: therotateApiKeyhelper inlib/control-plane/client.tswas dead AND wrong, decoding the top-level payload withdecodeApiKeywhen the handler answers{"old_key_id": ..., "new_key": {..., "secret": ...}}, so it would have thrown "Failed to parse API key response" for its first caller. It is deleted here rather than left as a trap.#1332 Credits shown as a bare integer on one page, dollars on another
The dashboard rendered
99,996,364,207with the unit only in the card title, while the API keys table rendered the same quantity as$0.000662. Settled on dollars, which is what the rest of the console already puts in front of a customer.One
CreditBalancecomponent now draws the balance on both the dashboard and the billing page: dollars as the headline, posted and reserved in the same denomination, and the credit figure with the conversion stated beside it, so the two surfaces reconcile by eye. Credits are kept because the ledger, invoices and per-key caps are denominated in them.Balances truncate toward zero rather than rounding (
formatUsdBalanceFromCredits), so 99,996,364,207 credits reads$99.99and never$100.00. Precision grows until the figure is non-zero, so a sub-cent balance reads$0.000662rather than an empty-looking$0.00. Currency presentation to BD customers is an unconstrained product decision per the owner ruling of 2026-08-08 (.wolf/decisions.mdD-035); no constraint is reintroduced here.Out of scope, worth a follow-up: the ledger, usage-log and invoice tables still print raw credit deltas.
Tests
New, and each one was checked against a mutated copy of the code it covers before being trusted:
lib/auth/self-serve.test.ts: the default matters most here, so unset, empty and unrecognized values all assert disabled.lib/auth/auth-error.test.ts: the three refusal shapes map to the refusal copy, an allow-listed message still renders verbatim, and a 500 still degrades to the generic message rather than being dressed up as a policy.__tests__/sign-up-next-redirect.test.tsx: no form when self-serve is off, the invitation copy and sign-in link render, thenextparam survives on the gated page, and a submitted signup that hits the refusal shows the refusal copy and not the outage copy.__tests__/sign-in-next-redirect.test.tsx: the cross-link appears when signup is open and is replaced by the invitation sentence when it is not.tests/unit/api-keys-list-actions.test.tsx: no rotate link, and no anchor pointing at a/rotatehref at all, with revoke still present.tests/unit/credit-balance.test.tsx: the headline metric is dollars and not a bare integer, and the credits plus conversion line is present.lib/format/format.test.ts: the balance formatter truncates, keeps sub-cent balances visible, and renders a single credit.Updated because the behaviour they pinned changed:
tests/e2e/unauth.spec.tsnow asserts the gated page (the CI stack builds with the flag unset, so it is disabled there),tests/interaction/route-floors.jsonrequires the sign-in link on that route instead of the removed form controls, andtests/e2e/demo-walkthrough.mjsreports the invitation posture as PASS rather than timing out on fields that are deliberately absent.Mutation check, all four together: the six touched suites produced 15 failures across every new group, then went green again on revert.
Verification
Run in Docker against a private image tag so the shared
hive-web-console:ciwas not rebuilt under other agents, and with--buildevery time, since the service mounts no volume:npm run test:unit: 832 passed, 76 files. The one failed file istests/unit/ci-web-e2e-secret-free.test.ts, which reads.github/workflows/ci.ymlfrom the repo root;Dockerfile.web-consoledoes not copy.github, so that file cannot pass inside this image on any branch. Pre-existing, unrelated to this diff.npm run build: green.Buglog entry
To be appended to
.wolf/buglog.jsonlon main after this merges, per the buglog protocol:{"date":"2026-08-29","area":"web-console","error_message":"Sign-up shows Something went wrong on our end. Reference AUTH-... after POST /auth/v1/signup returns a bare 404","root_cause":"The console shipped a sign-up route on deployments that refuse account creation at the gateway (Caddyfile.supabase 404) and at the GoTrue flag. The empty 404 body makes auth-js raise AuthUnknownError with no status, which the allow-list in lib/auth/auth-error.ts correctly withheld, so a deliberate policy rendered as an outage","fix":"Gate the sign-up route and its cross-links on NEXT_PUBLIC_DISABLE_SELF_SERVE_SIGNUP, fed from ENTERPRISE_DISABLE_SIGNUP so one variable drives GoTrue and the UI, and add toUserFacingSignUpMessage to report a 404, a 403, GoTrue signups-not-allowed copy, or the status-free AuthUnknownError shape as a policy refusal","tags":["web-console","auth","signup","issue-1328"]}{"date":"2026-08-29","area":"web-console","error_message":"Rotate on /console/api-keys opens This page could not be found","root_cause":"api-key-list.tsx linked every active key to /console/api-keys/[id]/rotate, a route that was never built; the page header also promised rotation. The rotateApiKey client helper was dead and decoded the wrong response shape","fix":"Remove the link and the promise, name the working path (create a replacement, revoke the old) in the header, and delete the broken helper","tags":["web-console","api-keys","issue-1331"]}{"date":"2026-08-29","area":"web-console","error_message":"Available credits renders 99,996,364,207 with no unit while the API keys table renders the same quantity as $0.000662","root_cause":"Two surfaces rendered the same credit quantity in different denominations, with no conversion shown on either","fix":"One CreditBalance component on the dashboard and the billing page, dollars as the headline with the credit figure and the conversion beneath, and a truncating formatUsdBalanceFromCredits so a balance never rounds up","tags":["web-console","billing","credits","issue-1332"]}Update after review
Rebased onto main to clear a conflict in
apps/web-console/lib/format/format.test.ts, which main had also touched. Worth recording why that mattered: while the PR was conflicting, GitHub built no merge ref, so the whole CI workflow never ran and the page showed only CodeQL, GitGuardian and CodeRabbit. That is the same trap the buglog protocol documents in issue #873, reached from a different direction.Three fixes landed after the first push, two of them from the review streams:
8.29 * 100printed $8.28.Final verification. In Docker, private image tag,
--buildon every run:npm run test:unit851 passed across 78 files, with the one pre-existing failure noted above (ci-web-e2e-secret-free.test.tscannot read.github/workflows/ci.ymlinside an image that does not copy.github).npm run buildgreen. On CI, 24 checks pass, includingWeb E2E (full stack), which exercises the gated sign-up route against a real built console with the flag unset, andInteraction coverage (console controls), which enforces the updated floor for that route.Visual proof was recaptured at the reviewed head so it shows the final code, and now covers the billing surface as well as the dashboard.