Repository navigation
fix(app-shell): the console asks GET /usage/storage only when the runtime serves features.storageUsage (objectui#11002) - #11609
Conversation
…time serves features.storageUsage (objectui#11002) On a self-hosted runtime an environment admin's console requested the cloud-only GET /api/v1/usage/storage on every shell mount and logged a 404. Per the ruling on the card (letter A), the cloud distribution serves features.storageUsage exactly when it mounts the endpoint (cloud#2481), and this side reads it default-off: - RuntimeFeatures gains optional storageUsage, defaulting to false and mapped by initRuntimeConfig as `=== true` beside customDomain / sso / scim. - isStorageUsageServed() (module-internal, like isAiStudioEnabled) is the one reading; StorageUsageBanner and ReadRateBanner both gate their hook on `isAdmin && isStorageUsageServed()`, so both banners still share one request when it is on and issue none when it is off. The exported hooks keep their `enabled = true` default. - The stale ConsoleShell comment that named useDiscovery() as the AI chatbot gate now names the context readers that are real. Pins: ConsoleShell.storageUsageGate-11002.test.tsx mounts the real shell against the real runtime-config singleton; runtime-config.test.ts pins the mapping. Changeset: @object-ui/app-shell minor, Clause-② yes (widening). Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
…eUsage pin (objectui#11002) `vi.stubGlobal` takes the value untyped, so the cast added nothing but a no-explicit-any warning the file did not carry before this branch. Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Read-only, against: the card (objectui#11002 body with its Check-runs on the head: first read 2026-10-04T10:43Z, 42 runs, 29 success, 3 skipped, 10 in progress; final read 2026-10-04T10:53Z, 43 runs, 40 success, 3 skipped, 0 failed, 0 in progress (the aggregate ① Derived judgments
② Semver level
③ Boundary flags
Nothing escalated. Implemented-by: VERDICT: PASS Written 2026-10-04T10:55Z. Judgments ① through ③ hold on |
|
Generated by Claude Code |
Fixes #11002
Clause-②: yes
The console asks
GET /api/v1/usage/storageonly when the runtime serves it. This applies the card's ruling5890841811(letter A) as written. The cloud distribution servesfeatures.storageUsage: trueexactly when its composition mounts the endpoint plugin. That half landed through objectstack-ai/cloud#2517, and cloudmain2205b53derives it from the plugin list. This side reads the flag default-off, through one boolean that both banners share.What publishes (
@object-ui/app-shell,minor)RuntimeFeaturesgains one optional member,storageUsage?: boolean.AppShellRuntimeConfig.features, and the valuegetRuntimeConfig()returns, carry it too.falseby default, and after any runtime-config payload whosefeatures.storageUsageis not the literaltrue. Absent,false,'true'and1all read asfalse; onlytruereads astrue. As with every feature key, a payload with nofeaturesobject leaves the current value.ConsoleShell. Its two banners request the endpoint only when the viewer is the environment admin, as before, and the flag is on. Both read the same flag and still share one request. A cloud runtime older than cloud#2517 sends no key, so its admins see neither banner until that runtime is upgraded.isStorageUsageServed()is module-internal, likeisMarketplaceEnabled()andisAiStudioEnabled(). It ships insidedist/but is not on the package entry, and theexportsmap has only.and./styles.css. Measured: the package entry re-exports neither the two banners noruseStorageUsageReading/useReadRateReading. The hooks are unchanged and keep theirenabled = truedefault.Changes
runtime-config.ts:RuntimeFeatures.storageUsagemember, with a docblock in its siblings' voice;storageUsage: falsein the defaults;storageUsage: body.features.storageUsage === trueininitRuntimeConfig, besidecustomDomain/sso/scim;isStorageUsageServed(), which fails closed (=== true) and readsfeatures?.. Its docblock records why: the opposite direction from the marketplace and AI Studio accessors, and never inferred from a 404.StorageUsageBannerandReadRateBanner: each passesenabled: isAdmin && isStorageUsageServed()and says so in its module header.ConsoleShell.tsx:useDiscovery()the AI chatbot's gate now names the readers that exist.useViewDataanduseElementDataSourcefall back toSchemaRendererContext'sdataSource. Measured: nothing in product source callsuseDiscovery.packages/app-shell/README.md(Storage capacity section) andcontent/docs/guide/console.md(the feature-flag list).The gate sits in the banners, not the hooks. The hooks are generic readers that already take
enabled; whether the shell's banner should ask is a fact about where the shell mounts it.Reproduction (red at base
278d244)The new
ConsoleShell.storageUsageGate-11002.test.tsxmounts the realConsoleShell, fills the real runtime-config singleton throughinitRuntimeConfig, and uses the real shared endpoint reader. OnlyuseWorkspaceAdminStatusand the network are stood in. Run against the base code it readsTests 2 failed | 2 passed (4). The no-key andfalsecases fail withexpected [ '/api/v1/usage/storage' ] to have a length of +0 but got 1. At base, the served case already shared ONE request between the two banners: that was measured here, not assumed.Pins
ConsoleShell.storageUsageGate-11002.test.tsx:storageUsagekey issues zero requests, and so does an admin withstorageUsage: false;storageUsage: trueissues exactly one request, shared by both banners. The storage banner rendersdata-storage-usage-case="warning"on thewarnverdict, and the read-rate banner renders on the same response;storageUsage: trueissues zero requests.runtime-config.test.ts, describeruntime-config storageUsage (objectui#11002):true;false,'true'and1;Ablations
Every mutation went through objectstack's
scripts/ablation-replace.mjs. The anchor hit once and was gone afterwards, and the blob hash changed. Each restore was proven by blob == HEAD and an emptygit diff HEAD, all from committed state.is ON when the server sends the literal true,withdraws it when a later fetch no longer serves it, the shell's ONE-request case (3 failed)false, so a dropped line cannot turn them=== truechanged to!!is OFF when the key is the string 'true',... the number 1(2 failed)StorageUsageBannerfalsecases, 1 request each (2 failed)ReadRateBannerisAdmin &&inStorageUsageBannerReverse check of the published type. After
pnpm --filter @object-ui/app-shell build,dist/runtime-config.d.tscarriesstorageUsage?: boolean, anddist/index.d.tsnamesRuntimeFeatureswith noisStorageUsageServed. A scratch probe resolved@object-ui/app-shellto that builtindex.d.ts(deleted after use):RuntimeFeatures['storageUsage'] = truecompiled (exit 0);= 'yes'failed withTS2322: Type '"yes"' is not assignable to type 'boolean | undefined'(exit 2).Gates
These ran at HEAD
27044c0unless noted. The second commit only drops one test-file cast, and the doc gates read files it does not touch.turbo run build --filter='@object-ui/app-shell^...' --concurrency=2(under the verify lock)Tasks: 28 successful, 28 total(ate45751f)pnpm --filter @object-ui/app-shell type-checktsc --noEmit && tsc -p tsconfig.test.json.--listFilesOnlyshows both new test files in the test programvitest runon the six touched suitesTest Files 6 passed (6),Tests 88 passed (88)vitest run --maxWorkers=3 packages/app-shell/plus the five source-text readers outside the packageTest Files 1001 passed, 1 skipped (1002),Tests 10076 passed, 9 skipped(ate45751f)eslint --no-inline-config --format jsonon the six changed.ts/.tsxfilesConsoleShell.tsx8 and 8,runtime-config.test.ts6 and 6pnpm check:control-bytesOK (scanned 7501 tracked text file(s)pnpm check:new-line-citations0 new citation(s)pnpm check:changeset-claimsNo pending changeset names a file this change touches.pnpm check:pending-changeset-literalsNo test source names a pending changeset.node scripts/check-changeset-presence.mjs6 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)pnpm changeset:check,check-changeset-overwrite.mjsNo changeset declares a major bump.,No pre-existing changeset was modified or deleted.check-vi-mock-specifiers/-override-shape/-inherit,check-test-path-rootsOKeachcheck:esm-specifiers,check:self-import,check:phantom-deps,check:unreferenced-sources,check:spec-symbols,check-type-check-coverage,check-lint-coveragee45751f)check-doc-links,check-doc-fence-languages,check-doc-component-types,check-doc-expression-carriage,check:doc-example-readers,check:docs-route-closuree45751f)pnpm check:readme-exportspackages/app-shell/README.mddraws 0 findings with app-shell's dist builtpnpm check:doc-examplesTHE GATE COULD NOT RUN, 12 packages outside this diff unbuilt. This diff adds no fenced block to any documentWhat the lint run covers. The population is the six changed source and test files, linted under the root
eslint.config.js, and the JSON output counts six. The narrowing is safe: the config enables no type-aware linting (noprojectServiceand noparserOptions.project), and no local rule ineslint-rules/reads the filesystem. So this diff cannot change a verdict on an untouched file. The repo-widepnpm lintbelongs to CI.The touched-suites run was selected two ways: by import (the importers of
runtime-config.ts, the two banners andConsoleShell.tsxsit inconsole/,layout/,chrome/andobservability/), and by source-text read (a grep forreadFileSync,?rawand the file names). The full-package run covers both.Acceptance notes
StorageUsageBanner.test.tsxandReadRateBanner.test.tsxmock their hook. Their admin cases now hand the mockenabled: false, because the runtime-config default is off, and the mock answers with a reading anyway. They stay green and test the classifier, not the gate. The shell pins above test the gate. carrier: none.isStorageUsageServed()is read at render, likeisAiStudioEnabled(), and the console awaitsinitRuntimeConfigbefore the first render. A later re-init does not re-render the banners by itself. That is unchanged sibling behaviour, not a new gap. carrier: none.Session:
https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2Generated by Claude Code