Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions docs/proof/chat-settings-usage-tab-2026-08-28/capture.log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Visual proof capture log: chat settings retitle + Usage tab

PR: feat/chat-settings-usage-tab. Captured 2026-08-28.

## What is proven

Two screenshots, captured with Playwright (chromium) against a real,
locally-built Docker image (`docker build -f deploy/docker/Dockerfile.open-webui
...`, the actual shipped Dockerfile's backend-patch pipeline and its own final
integrity assertion all ran and passed against this build; only the frontend
compile stage substituted a direct `npx vite build` for `npm run build`
because this sandbox cannot reach the pyodide asset CDN — see "What could not
be verified" below), running as a real container, reached over plain HTTP on
`localhost` with no mocking of the rendered UI.

- `proof-general-chat-preferences.png`: Settings modal, General tab. Header
reads "Chat Preferences" (was the literal upstream string "WebUI Settings",
the exact parity-review finding). Tab rail: General, Account, Usage,
Interface, Audio, Data Controls, About — Usage clustered next to Account,
mirroring the Claude Desktop reference's grouping named in the task.
- `proof-usage-tab.png`: Settings modal, Usage tab (new). Shows "Usage isn't
available on this deployment.", a working "Refresh" control, and a "Last
updated" timestamp. This is the honest, designed fallback state, not a
placeholder: `hive_credits.py` (the OWUI-side proxy) fails closed to 404
when its own upstream (control-plane's credits endpoint) is unreachable,
and `Usage.svelte` renders that as this explicit sentence rather than a
blank panel or a spinner stuck forever. See "What could not be verified"
for why the balance itself could not be exercised in this run.

## Sign-in path used

Not the shared QA fixture (`.env` lines 70-71) and not the production OAuth
("Continue with Hive") flow. This local verification stack's `open-webui`
service had `ENABLE_SIGNUP` and `ENABLE_LOGIN_FORM` overridden to `true` for
this run only (via an untracked, uncommitted `docker-compose.verify-override.yml`,
deleted after use), and a brand-new, throwaway account (`verify-<epoch
millis>@example.com`, random per-run password) was created through OWUI's own
native signup form, the first such account on a freshly created `owui-data`
volume becomes the local instance's own admin. No shared credential was read,
touched, or rotated.

## What could not be verified, and why (environment, not code)

Full live-data verification (a Usage tab showing a real non-zero credit
balance) was blocked by a pre-existing, environment-wide problem, confirmed
independently before touching anything:

- The shared `.env`'s `SUPABASE_URL` (`https://yimgflllgdsbcibnaxqe.supabase.co`)
does not resolve at all (`curl: (6) Could not resolve host`). The Supabase
Cloud project it names was deleted during the self-hosted cutover
(`.wolf/decisions.md`, project self-hosted-Supabase-migration notes).
- `SUPABASE_DB_URL` points at the same dead project's pooler
(`aws-1-us-east-1.pooler.supabase.com`); it answers with a real Postgres
wire-protocol error, `FATAL: (ENOTFOUND) tenant/user
postgres.yimgflllgdsbcibnaxqe not found`, confirming the project reference
itself is gone, not a transient network issue.
- This is not specific to this run or this sandbox: other agents'
concurrently running `control-plane` containers on this same shared box
were independently observed in the same `unhealthy` state before this
session touched anything, using the same shared `.env`.
- The real, live self-hosted GoTrue is reachable and healthy at
`https://console-hive.scubed.co/auth/v1/health` (HTTP 200), confirming the
production deployment itself is fine; only the local `.env`'s pointers are
stale.

Given this, exercising the real `internal/chat/credits/balance` code path
(which needs a live Postgres connection to resolve tenant -> account ->
balance) was not achievable from this sandbox without fabricating a database
connection string, which was not done. The captured "Usage isn't available on
this deployment." screenshot is the correct, honest behavior of this exact
condition, not a placeholder standing in for something unverified. The
formatter that would render a real balance (`formatUsdFromCredits`, ported
from `apps/web-console/lib/format/model-pricing.ts`) is independently unit
tested end to end in `vendor/open-webui/src/lib/hive/credits.test.ts`,
including the explicit "never renders a bare integer, zero renders `$0`"
invariant.

Also not verified live: OAuth sign-in through the production "Continue with
Hive" flow, since `OPENID_PROVIDER_URL` in this deployment derives from the
same dead `SUPABASE_URL`.

## Full test and build evidence (reproducible, not just this capture)

- `npm run test:frontend -- --run` inside `vendor/open-webui`: 208/208 tests
passed across 16 files, including the 10 new regression tests in
`src/lib/hive/settings-usage-tab.test.ts`.
- `npx vite build` (real production compile of the full SvelteKit app,
including `Usage.svelte` and the `ChartBar` icon import): succeeded,
produced `build/index.html` and every real chunk (`ChartBar.js` present in
the emitted chunk list).
- `docker build -f deploy/docker/Dockerfile.open-webui ...` (the real,
shipped Dockerfile, unmodified except substituting the frontend build
command for the reason above): succeeded, including every one of the
Dockerfile's ~50 backend-patch and assertion steps, ending in its own final
integrity check: `hive: shell present, removed surfaces absent`.

## Credential handling

No credential-bearing URL was captured in any screenshot (headless Playwright
screenshots in this run capture only page content, not browser chrome or the
address bar). The throwaway signup email/password are not secrets (random,
single-use, `example.com`/`example.invalid`, never touch any real system) and
are not redacted for that reason.
135 changes: 135 additions & 0 deletions docs/proof/chat-settings-usage-tab-2026-08-29/capture.log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Visual proof capture log: Usage tab, populated state and enterprise absence

PR: feat/chat-settings-usage-tab, second capture. Captured 2026-08-29.

Supersedes the 2026-08-28 capture in the sibling directory, which proved the
General retitle but showed the Usage tab in its no-data fallback rather than
the claimed "credit balance and today's usage". That gap was the review
finding this capture closes.

## What is proven

Three screenshots, taken with Playwright (chromium, 1280x860) against a real
container built from this branch with the shipped
`deploy/docker/Dockerfile.open-webui`, reached over plain HTTP on localhost.
No stubbing of the rendered UI: the only thing intercepted is the one HTTP
response the tab consumes.

- `20-settings-general.png`: Settings, General tab. Header reads "Chat
Preferences", the retitle. Tab rail reads General, Account, Usage,
Interface, Audio, Data Controls, About, with Usage clustered next to
Account.
- `21-settings-usage.png`: Settings, Usage tab, populated. "Organization
credit balance $12.50", "Organization usage today $0.34", the "Top up"
link, a "Last updated" stamp and the "Refresh" control. Both money labels
carry the organization scope, which is the relabel from the review: the
figures are tenant scope and previously read as personal.
- `22-settings-enterprise.png`: the same build with the credits endpoint
answering its documented 404. The Usage entry is absent from the rail
entirely (General, Account, Interface, Audio, Data Controls, About), which
is the silent absence posture `deploy/docker/owui-patches/hive_credits.py`
describes. Before this change the tab was present and permanently empty.

## How the populated state was produced without a database

Issue #1297 leaves this sandbox with no reachable Supabase, so the real
`internal/chat/credits/balance` chain cannot run here. It does not have to:
the browser sees exactly one credits response, and Playwright fulfils it.

```js
await page.route('**/hive/credits/balance', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
available_credits: 12500000000,
usage_today_credits: 340000000,
top_up_url: 'https://console-hive.scubed.co/console/billing'
})
});
});
```

The two magnitudes are deliberately different, so the screenshot also shows
the figures landing in the right rows: 12,500,000,000 credits is $12.50 at
the D-046 rate of one billion credits per dollar, and 340,000,000 credits is
$0.34. A transposition would be visible in the image itself. The third
capture uses the same route with `status: 404` and the endpoint's own body,
`{"detail": "Credits are unavailable."}`.

The panel's rendered text, read back from the live DOM in the same run:

```
Usage | Organization credit balance | $12.50 | Organization usage today |
$0.34 | Top up | Last updated: 11:07:13 PM | Refresh
```

Tab rails, read from the live DOM in the same run:

```
credits available: ["General","Account","Usage","Interface","Audio","Data Controls","About"]
credits absent: ["General","Account","Interface","Audio","Data Controls","About"]
```

## Sign-in path used

Not the shared QA fixture and not the production OAuth flow. The container is
a throwaway with its own empty volume, `ENABLE_SIGNUP` and
`ENABLE_LOGIN_FORM` on for this run only, and the first account created on it
becomes its own local admin. That account was created through the container's
own signup endpoint with a random single use password at an `example.invalid`
address, used only against this container, and destroyed with it. Nothing
about it is written here, no shared credential was read, touched or rotated,
and no URL in any screenshot carries a token.

## Build note, environment rather than code

The image is the shipped Dockerfile with one substitution: the frontend build
step runs `npx vite build` instead of `npm run build`, because
`npm run pyodide:fetch` cannot reach the pyodide CDN from this sandbox and
fails the build outright (`fetch failed`, `SocketError: other side closed`,
observed twice). Everything else ran unmodified, including the stage that
runs `npm run test:frontend -- --run` against the real vendored node_modules,
which reported 16 files and 221 tests passed. That is the in place run of the
same test sources the pre-merge gate runs in its scratch tree, so the render
assertions added in this PR are confirmed to work in both places.

The window chrome in these captures reads "Open WebUI" rather than the Hive
name because this standalone container is run without the compose file's
branding environment. It has no bearing on what is being proven.

## Test evidence backing the same change

`make test-owui-frontend`, the pre-merge gate, on the unmutated tree:

```
Test Files 16 passed (16)
Tests 221 passed (221)
14/14 components compiled
```

The same gate, with each of the reviewer's three mutations applied one at a
time: compile error red, transposed figures red on three assertions, emptied
click handler red on one assertion, exit 2 in every case.

## Recaptured after the second review round

The screenshots above were first taken at commit `6a30d39`, then retaken
unchanged in appearance at `338b699`, the head that answers the second review
round. That round changed how the number reaches the panel: the settings
modal's availability probe now hands its balance and fetch time straight to
the Usage panel instead of the panel firing its own request, and the probe is
serialized so two opens cannot land out of order. The panel therefore renders
the same figures from a snapshot rather than from its own mount fetch, which
is why this capture was redone rather than reused.

Same run, read back from the live DOM at that head:

```
Usage | Organization credit balance | $12.50 | Organization usage today |
$0.34 | Top up | Last updated: 11:30:24 PM | Refresh
```

The image build for that container reported `Test Files 16 passed (16)` and
`Tests 221 passed (221)` from its in place `npm run test:frontend -- --run`
stage, against the same commit.
70 changes: 57 additions & 13 deletions scripts/test-owui-hive-frontend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ cp "$ROOT/vendor/open-webui/src/app.html" "$WORK"/app.html
# flattened, so the mirroring described above still holds.
cp -R "$SRC"/. "$WORK"/lib/hive/

# The settings declutter guard pins the rendered surface of chat components,
# plus the layout/page files that also forward directConnections, by reading
# their sources.
# The settings declutter guard (plus the settings retitle/Usage-tab guard)
# pins the rendered surface of chat components, plus the layout/page files
# that also forward directConnections, by reading their sources.
COMPONENT_SRC="$ROOT/vendor/open-webui/src/lib/components"
for rel in \
chat/SettingsModal.svelte \
chat/ModelSelector/Selector.svelte \
chat/Settings/Account.svelte \
chat/Settings/General.svelte \
chat/Settings/Advanced/AdvancedParams.svelte \
chat/MessageInput.svelte \
chat/Chat.svelte \
Expand All @@ -58,6 +59,19 @@ do
cp "$COMPONENT_SRC/$rel" "$WORK/lib/components/$rel"
done

# The two locale catalogues the settings guard reads. en-US is the key
# catalogue every other locale is generated from, and bn-BD is the first
# market, so a rename that silently drops a translated string fails here
# rather than in front of a Bangladeshi customer. Only these two travel: the
# other 61 are never asserted against and copying them would cost seconds per
# run for nothing.
I18N_SRC="$ROOT/vendor/open-webui/src/lib/i18n/locales"
for rel in en-US bn-BD
do
mkdir -p "$WORK/lib/i18n/locales/$rel"
cp "$I18N_SRC/$rel/translation.json" "$WORK/lib/i18n/locales/$rel/translation.json"
done

ROUTES_SRC="$ROOT/vendor/open-webui/src/routes"
for rel in \
+layout.svelte \
Expand All @@ -78,6 +92,24 @@ cp "$ROOT/scripts/owui-hive-svelte-compile-check.mjs" "$WORK"/
# still needs no host node, per the Docker-only testing contract above.
cp "$ROOT/vendor/open-webui/package-lock.json" "$WORK"/owui-package-lock.json

# A vitest config for the scratch tree, so a test can IMPORT a Hive component
# and assert what it renders rather than only reading its source as text. The
# in-place run (npm run test:frontend, Dockerfile.open-webui) gets the same
# capability from vite.config.ts's sveltekit() plugin; this is the scratch
# tree's equivalent, and both compile with the same pinned svelte, so a test
# that renders behaves identically in both places. Node environment on purpose:
# the render assertions use svelte/server, which needs no DOM, so nothing here
# depends on a jsdom the vendored lockfile does not carry.
cat > "$WORK"/vitest.config.mjs <<'CONFIG'
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vitest/config';

export default defineConfig({
plugins: [svelte()],
test: { environment: 'node' }
});
CONFIG

cd "$WORK"

# Runs in a pinned node image rather than on host node, per CLAUDE.md's
Expand Down Expand Up @@ -108,24 +140,36 @@ docker run --rm \
# drift. Both are installed into the scratch tree rather than pulled
# through npx: vitest resolves @vitest/coverage-v8 relative to the project
# root it runs from, and packages fetched through separate npx prefixes are
# invisible to that lookup. Same pattern as the svelte install below.
# invisible to that lookup.
# The text reporter prints the per-file table plus an All files total line:
# advisory measurement for this scratch-tree run, no thresholds.
# Scoped to lib/hive, not all of lib: the upstream components and routes
# copied in above are text fixtures the declutter guard reads, not code
# this suite executes, so including them would drag the total down with
# permanently-zero rows.
npm install --no-save --no-audit --no-fund --loglevel=error vitest@2 @vitest/coverage-v8@2
npx vitest run --coverage --coverage.include="lib/hive/**" --coverage.reporter=text
svelte_version=$(node -e "
#
# Svelte and its vite plugin are installed BEFORE the test run, not after
# it, because the tests now import Hive components and render them; the
# compile pass below reuses the same install. Both versions come from the
# vendored lockfile so this check runs the EXACT versions the image build
# resolves. A major-only pin would let it pass with a different 5.x than
# deploy/docker/Dockerfile.open-webui uses, which is a fresh way to get a
# green check and a red deploy.
pinned=$(node -e "
const lock = require(\"/work/owui-package-lock.json\");
const entry = lock.packages && lock.packages[\"node_modules/svelte\"];
if (!entry || !entry.version) {
console.error(\"svelte absent from vendor/open-webui/package-lock.json\");
const pkgs = lock.packages || {};
const svelte = pkgs[\"node_modules/svelte\"];
const plugin = pkgs[\"node_modules/@sveltejs/vite-plugin-svelte\"];
if (!svelte || !svelte.version || !plugin || !plugin.version) {
console.error(\"svelte or its vite plugin absent from vendor/open-webui/package-lock.json\");
process.exit(1);
}
process.stdout.write(entry.version);
process.stdout.write(svelte.version + \" \" + plugin.version);
")
echo "compiling components with svelte@$svelte_version, the version the image build resolves"
npm install --no-save --no-audit --no-fund --loglevel=error "svelte@$svelte_version"
svelte_version=${pinned% *}
plugin_version=${pinned#* }
echo "pinning svelte@$svelte_version and @sveltejs/vite-plugin-svelte@$plugin_version, the versions the image build resolves"
npm install --no-save --no-audit --no-fund --loglevel=error \
vitest@2 @vitest/coverage-v8@2 "svelte@$svelte_version" "@sveltejs/vite-plugin-svelte@$plugin_version"
npx vitest run --coverage --coverage.include="lib/hive/**" --coverage.reporter=text
node owui-hive-svelte-compile-check.mjs lib/hive'
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,17 @@
<div class="flex flex-col h-full justify-between text-sm" id="tab-general">
<div class=" overflow-y-scroll max-h-[28rem] md:max-h-full">
<div class="">
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div>
<!-- hive: was "WebUI Settings" (parity review finding, this section
read as stock Open WebUI branding rather than Hive's own product).
The key equals its own English text, the i18next convention this
file already relies on, so every locale renders something
immediately. That is a fallback, not a translation: the old key was
translated in bn-BD and 61 other locales and the rename drops those
translations, so the new key is added to en-US and translated in
bn-BD (the first market) in this same change. The remaining locales
fall back to English until their own translators reach it, which is
how every other untranslated key in this fork already behaves. -->
<div class=" mb-1 text-sm font-medium">{$i18n.t('Chat Preferences')}</div>
Comment thread
sakibsadmanshajib marked this conversation as resolved.

<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Theme')}</div>
Expand Down
Loading
Loading