Skip to content

fix: make three console surfaces state what the deployment actually does - #1336

Merged
sakibsadmanshajib merged 5 commits into
mainfrom
fix/console-honest-surfaces
Aug 29, 2026
Merged

sakibsadmanshajib merged 5 commits into
mainfrom
fix/console-honest-surfaces

Conversation

@sakibsadmanshajib

@sakibsadmanshajib sakibsadmanshajib commented Aug 29, 2026 •

Copy link
Copy Markdown
Owner

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.supabase answers 404 for /auth/v1/signup on the public listener, and hive-supabase-auth-1 runs with GOTRUE_DISABLE_SIGNUP=true. Re-verified live while writing this: POST https://console-hive.scubed.co/auth/v1/signup returns HTTP/2 404 with content-length: 0 and via: 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 AuthUnknownError whose message is a JSON parse error and whose status is undefined. The allow-list in lib/auth/auth-error.ts withheld 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.ts reads NEXT_PUBLIC_DISABLE_SELF_SERVE_SIGNUP and fails closed. Unset and empty both mean disabled, because an unset build arg reaches next build as an empty string.
  • The build arg is fed from 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-up renders 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.
  • toUserFacingSignUpMessage reports 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-free AuthUnknownError shape 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.md D-023.

Turnstile was ruled out before this work and is not involved: NEXT_PUBLIC_TURNSTILE_SITE_KEY is empty on the console container and TURNSTILE_SECRET_KEY is unset on control-plane.

#1331 Rotate is a dead link

/console/api-keys/[id]/rotate does 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: the rotateApiKey helper in lib/control-plane/client.ts was dead AND wrong, decoding the top-level payload with decodeApiKey when 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,207 with 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 CreditBalance component 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.99 and never $100.00. Precision grows until the figure is non-zero, so a sub-cent balance reads $0.000662 rather 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.md D-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, the next param 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 /rotate href 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.ts now asserts the gated page (the CI stack builds with the flag unset, so it is disabled there), tests/interaction/route-floors.json requires the sign-in link on that route instead of the removed form controls, and tests/e2e/demo-walkthrough.mjs reports 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:ci was not rebuilt under other agents, and with --build every time, since the service mounts no volume:

  • npm run test:unit: 832 passed, 76 files. The one failed file is tests/unit/ci-web-e2e-secret-free.test.ts, which reads .github/workflows/ci.yml from the repo root; Dockerfile.web-console does 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.jsonl on 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:

  • The balance formatter truncates in credit space rather than dollar space. Truncating 8.29 * 100 printed $8.28.
  • A non-finite credit value renders as an em dash rather than $0.00, so a failed read cannot look like an empty wallet.
  • The refusal classifier keeps its rules, with the measurement that justifies them recorded beside it and asserted by the test.

Final verification. In Docker, private image tag, --build on every run: npm run test:unit 851 passed across 78 files, with the one pre-existing failure noted above (ci-web-e2e-secret-free.test.ts cannot read .github/workflows/ci.yml inside an image that does not copy .github). npm run build green. On CI, 24 checks pass, including Web E2E (full stack), which exercises the gated sign-up route against a real built console with the flag unset, and Interaction 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.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 39 minutes.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fc1ddbeb-1823-44aa-9a8c-6dc41a58274e

📥 Commits

Reviewing files that changed from the base of the PR and between 3b2bffd and 363cbcf.

📒 Files selected for processing (26)
  • .env.example
  • apps/web-console/__tests__/sign-in-next-redirect.test.tsx
  • apps/web-console/__tests__/sign-up-next-redirect.test.tsx
  • apps/web-console/app/auth/sign-in/page.tsx
  • apps/web-console/app/auth/sign-up/page.tsx
  • apps/web-console/app/console/api-keys/page.tsx
  • apps/web-console/app/console/page.tsx
  • apps/web-console/components/api-keys/api-key-list.tsx
  • apps/web-console/components/billing/billing-overview.tsx
  • apps/web-console/components/billing/credit-balance.tsx
  • apps/web-console/lib/auth/auth-error.test.ts
  • apps/web-console/lib/auth/auth-error.ts
  • apps/web-console/lib/auth/self-serve.test.ts
  • apps/web-console/lib/auth/self-serve.ts
  • apps/web-console/lib/control-plane/client.ts
  • apps/web-console/lib/format/credits.ts
  • apps/web-console/lib/format/format.test.ts
  • apps/web-console/lib/format/model-pricing.ts
  • apps/web-console/tests/e2e/demo-walkthrough.mjs
  • apps/web-console/tests/e2e/unauth.spec.ts
  • apps/web-console/tests/interaction/route-floors.json
  • apps/web-console/tests/unit/api-keys-list-actions.test.tsx
  • apps/web-console/tests/unit/credit-balance.test.tsx
  • deploy/docker/Dockerfile.web-console.prod
  • deploy/docker/docker-compose.yml
  • docs/proof/console-honest-surfaces-2026-08-29/capture-log.md

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Visual proof

Captured 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

pr1336-20260829022456-8251-01-dashboard-credits.png

pr1336-20260829022459-20381-02-api-keys-no-rotate.png

pr1336-20260829022502-27158-03-signup-by-invitation.png

pr1336-20260829022505-7554-04-signin-no-create-link.png

@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Adversarial review

Streams

  • Antigravity (agy, gemini-3.1-pro-high, effort high): RAN. Five findings, all answered below. The first attempt timed out at twenty minutes with no output; the second, scoped to the changed files, returned.
  • CodeRabbit CLI: SKIPPED. coderabbit review --agent --base main returned rate_limit, "You have used all 3 included reviews currently available". Not a pass.
  • CodeRabbit bot on this PR: SKIPPED for now. It posted "Review limit reached, next included review available in 38 minutes". I will re-trigger it once the window opens.
  • Codex: SKIPPED. The connector replied "You have reached your Codex usage limits for code reviews", which matches the known exhaustion until 2026-09-10.

Findings and dispositions

  1. NEXT_PUBLIC_DISABLE_SELF_SERVE_SIGNUP is baked into the bundle at build time, so a prebuilt image cannot be flipped at runtime. Major. Rebutted, with a note. This is true of the mechanism and it is the same mechanism every other browser-visible setting in this image already uses: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_APP_URL and NEXT_PUBLIC_TURNSTILE_SITE_KEY are all build args on the same Dockerfile. It also costs nothing operationally here: this repo ships no prebuilt console image, every deploy path runs docker compose up -d --build, and re-enabling self-serve signup is not a one-variable change in the first place. It also needs the @selfserve refusal lifted in deploy/docker/Caddyfile.supabase, which is a repo edit and therefore a rebuild regardless. Moving the read to a server component would additionally force dynamic = "force-dynamic" on two auth pages that are otherwise static shells, to buy an operation nobody performs.
  2. isSignUpRefusal misclassifies outages as policy. Major. Rebutted with a measurement, and the measurement is now recorded in the code. Driving supabase.auth.signUp against a stubbed fetch on @supabase/auth-js 2.x, 2026-08-29: 500, 502, 503 and 504 with an HTML body all produce AuthRetryableFetchError with the status preserved, and a dead transport produces the same class with status 0. Only a non-5xx status with a body that is not JSON produces AuthUnknownError, which is exactly the empty-bodied 404 this gateway returns (content-length: 0, verified live against the deployed origin). No outage shape auth-js recognises can reach that branch. The test now asserts the real AuthRetryableFetchError shapes rather than an invented one, so the claim is pinned rather than argued.
  3. A non-finite balance renders as $0.00, so a failed read looks like an empty wallet. Major. Accepted and fixed in 661febe. formatUsdBalanceFromCredits now returns the em dash for a non-finite value, matching formatPercent in the same module, which exists for exactly this reason. Zero still renders as $0.00, because zero is a real balance. The upstream decode still collapses a missing field to 0 (readNumberField(...) ?? 0 in lib/control-plane/client.ts), which is pre-existing and out of scope here.
  4. Float truncation shaves a cent off exact values. Major. Accepted, and already fixed in b29fdd7 before this stream returned; found independently in self review. 8,290,000,000 credits is 8.29 dollars and 8.29 * 100 is 828.9999999999999, so truncating in dollar space printed $8.28. The truncation now happens in credit space, where CREDITS_PER_USD / 10 ** digits is an exact integer. The regression test fails on the old implementation, which was verified by reverting it.
  5. The client boundary sits at the top of the route. Minor. Rebutted. app/auth/sign-up/page.tsx was already a client component before this change, and the same is true of sign-in. Moving the boundary is a refactor of both auth pages that this diff has no reason to carry, and it would not change where the flag is read, since NEXT_PUBLIC_* is inlined at build time either way. See finding 1.

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.
@sakibsadmanshajib
sakibsadmanshajib force-pushed the fix/console-honest-surfaces branch from 661febe to ba3bc21 Compare August 29, 2026 02:47
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Visual proof

Recaptured 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

pr1336-20260829025318-6395-01-dashboard-credits.png

pr1336-20260829025321-19345-05-billing-balance.png

pr1336-20260829025324-21916-02-api-keys-no-rotate.png

pr1336-20260829025328-5740-03-signup-by-invitation.png

pr1336-20260829025331-22578-04-signin-no-create-link.png

@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 10 minutes.

@sakibsadmanshajib
sakibsadmanshajib merged commit ddef640 into main Aug 29, 2026
31 checks passed
@sakibsadmanshajib
sakibsadmanshajib deleted the fix/console-honest-surfaces branch August 29, 2026 03:13
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 39 minutes.

@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Review status, final

Streams

  • Antigravity (agy, gemini-3.1-pro-high): RAN, five findings, all answered in the comment above. Two accepted and fixed, three rebutted with reasons.
  • CodeRabbit: SKIPPED, and not a pass. Three separate attempts: the CLI answered rate_limit ("all 3 included reviews currently available" used), and the bot answered "Review limit reached" at 02:12, again at 02:54 after the first @coderabbitai full review, and again at 03:25 after the second. It has posted no inline comment on this PR at any point.
  • Codex: SKIPPED, and not a pass. "You have reached your Codex usage limits for code reviews", the known exhaustion.

Two more fixes since that comment, both from my own re-read of the final diff

  • Math.floor rather than Math.trunc when rounding a balance. available_credits is posted minus reserved, so a workspace whose holds exceed its posted credits reads negative, and rounding toward zero showed less of the hole than was there. Identical for every positive balance, conservative for a negative one, with a test.
  • The docstring now states the rounding direction rather than "toward zero", so the comment cannot drift from the code.

Threads. There are no inline review comments on this PR (GET /pulls/1336/comments returns an empty list), so there is nothing to resolve. Every finding that was raised, was raised in a stream summary and is answered in this thread.

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@sakibsadmanshajib, thanks for the final status.

The Math.floor change correctly preserves the full negative balance after denomination rounding. The updated docstring now matches that behavior.

No CodeRabbit inline review thread requires resolution.

🐇


✏️ Learnings added
Learnt from: sakibsadmanshajib
URL: https://github.com/sakibsadmanshajib/hive/pull/1336

Timestamp: 2026-08-29T03:38:00.885Z
Learning: In `apps/web-console/lib/format/format.ts`, `formatUsdBalanceFromCredits` must use `Math.floor` when it truncates a USD balance. `available_credits` can be negative when reserved credits exceed posted credits. `Math.trunc` would round a negative balance toward zero and understate the deficit. Positive balances have identical results with `Math.floor`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

sakibsadmanshajib added a commit that referenced this pull request Aug 29, 2026
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"]}
```
sakibsadmanshajib added a commit that referenced this pull request Aug 29, 2026
…#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>
sakibsadmanshajib added a commit that referenced this pull request Aug 29, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant