From e45751fe684e21d8757cb9ae5d4a0bf3709dda88 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 09:58:42 +0000 Subject: [PATCH 1/2] fix(app-shell): the console asks GET /usage/storage only when the runtime serves features.storageUsage (objectui#11002) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .changeset/11002-storage-usage-flag.md | 11 ++ content/docs/guide/console.md | 2 +- packages/app-shell/README.md | 7 + .../app-shell/src/console/ConsoleShell.tsx | 12 +- ...nsoleShell.storageUsageGate-11002.test.tsx | 165 ++++++++++++++++++ .../app-shell/src/layout/ReadRateBanner.tsx | 11 +- .../src/layout/StorageUsageBanner.tsx | 12 +- packages/app-shell/src/runtime-config.test.ts | 59 ++++++- packages/app-shell/src/runtime-config.ts | 47 ++++- 9 files changed, 316 insertions(+), 10 deletions(-) create mode 100644 .changeset/11002-storage-usage-flag.md create mode 100644 packages/app-shell/src/console/__tests__/ConsoleShell.storageUsageGate-11002.test.tsx diff --git a/.changeset/11002-storage-usage-flag.md b/.changeset/11002-storage-usage-flag.md new file mode 100644 index 0000000000..530ee875ea --- /dev/null +++ b/.changeset/11002-storage-usage-flag.md @@ -0,0 +1,11 @@ +--- +'@object-ui/app-shell': minor +--- + +The console asks `GET /api/v1/usage/storage` only when the runtime serves it (objectui#11002). On a self-hosted or open-source runtime, an environment admin's console used to request that cloud-only endpoint on every shell mount and log a 404 each time. Now neither the storage-capacity banner nor the read-rate report asks unless the runtime config's `features.storageUsage` is `true`. The cloud distribution sends that key exactly when it mounts the endpoint (cloud#2481), and every other runtime sends no key. + +**Clause-②: yes (widening)** — the exported `RuntimeFeatures` type gains one optional member, `storageUsage?: boolean`, so `AppShellRuntimeConfig.features` and the value `getRuntimeConfig()` returns carry it too. It is `false` by default and after any runtime-config payload whose `features.storageUsage` is not the literal `true` (absent, `false`, `'true'` and `1` all read as `false`). It is `true` only when the payload sends `true`. As with the other feature keys, a payload with no `features` object leaves the current value. Nothing else on the package entry changes: no export is added or removed, and no existing member changes type. + +**Behaviour change.** `ConsoleShell`'s two banners request the endpoint only when the viewer is the environment admin, as before, and `features.storageUsage` is on. Both read the same flag and still share one request. A cloud runtime older than the one that serves the key (cloud#2517) sends no key, so its admins see neither banner until that runtime is upgraded. + +Not published: the accessor the banners read, `isStorageUsageServed()`. Like its siblings `isMarketplaceEnabled()` and `isAiStudioEnabled()`, it ships inside `dist/` but is not exported from the package entry. `useStorageUsageReading` and `useReadRateReading` are unchanged and keep their `enabled = true` default. diff --git a/content/docs/guide/console.md b/content/docs/guide/console.md index bc6a054dbc..5310f54558 100644 --- a/content/docs/guide/console.md +++ b/content/docs/guide/console.md @@ -95,7 +95,7 @@ Vite dev server proxies `/api/*` to the backend. See [Running with a Real Backen **2. Server-pushed runtime config — everything else.** Before React mounts, the console resolves `/api/v1/runtime/config` from that server and applies it: product branding, feature -flags (marketplace, AI Studio, SSO, custom domain), and the cloud URL. Operators configure +flags (marketplace, AI Studio, SSO, custom domain, the storage-usage reading), and the cloud URL. Operators configure these on the **server**, not in the SPA, which is why changing them needs no console rebuild. Apps, objects and views themselves are metadata fetched over HTTP — discovered at connect diff --git a/packages/app-shell/README.md b/packages/app-shell/README.md index 37e2530193..db3e8000a8 100644 --- a/packages/app-shell/README.md +++ b/packages/app-shell/README.md @@ -186,6 +186,13 @@ Both banners read that endpoint through one shared reader, so a page load issues one request for the two of them. The storage banner has the same audience and the same gate as the read-rate report. +Neither banner asks a runtime that does not serve that endpoint. Both read one +flag from the server-pushed runtime config, `features.storageUsage`, and ask +only when it is `true` (objectui#11002). The cloud distribution sends it +exactly when it mounts the endpoint; every other runtime sends no key, which +reads as off, so a self-hosted admin's page load issues no request and logs no +404. Only the literal `true` turns it on. + ## Components ### AppShell diff --git a/packages/app-shell/src/console/ConsoleShell.tsx b/packages/app-shell/src/console/ConsoleShell.tsx index 100aa89968..1715d826d5 100644 --- a/packages/app-shell/src/console/ConsoleShell.tsx +++ b/packages/app-shell/src/console/ConsoleShell.tsx @@ -182,11 +182,14 @@ function ConsoleShellProviders({ children }: { children: ReactNode }) { report. Renders null unless the control plane's verdict is `anomalous`, so an ordinary session and an unmeasured environment both see nothing — and a non-admin session - never even issues the request. */} + never even issues the request, nor does any session on a + runtime whose config does not serve `storageUsage` + (objectui#11002). */} {/* objectui#10439 — the environment admin's storage-capacity banner, beside the read-rate report and behind the same - admin gate; the two read one `/usage/storage` response. + two gates (the admin, and the runtime serving + `storageUsage`); the two read one `/usage/storage` response. Renders null unless the runtime's own verdict says `warn` (80% and up) or `blocked` (uploads and imports refused). */} @@ -284,8 +287,9 @@ function ConnectedShellInner({ children }: { children: ReactNode }) { if (adapter) lastLanguage.current = language; if (!adapter || isAuthLoading) return ; - // Expose the adapter via SchemaRendererContext so descendant hooks like - // useDiscovery() (used to gate the global AI chatbot) can resolve it. + // Expose the adapter via SchemaRendererContext so descendant hooks that fall + // back to the context's `dataSource` (`useViewData`, `useElementDataSource`) + // can resolve it. return ( diff --git a/packages/app-shell/src/console/__tests__/ConsoleShell.storageUsageGate-11002.test.tsx b/packages/app-shell/src/console/__tests__/ConsoleShell.storageUsageGate-11002.test.tsx new file mode 100644 index 0000000000..325833fae8 --- /dev/null +++ b/packages/app-shell/src/console/__tests__/ConsoleShell.storageUsageGate-11002.test.tsx @@ -0,0 +1,165 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * `ConsoleShell` asks `GET /api/v1/usage/storage` only when the runtime serves + * it (objectui#11002, ruled on the card as letter A). + * + * The endpoint is a cloud one: the cloud distribution's `RuntimeConfigPlugin` + * serves `features.storageUsage: true` exactly when its composition mounts the + * endpoint plugin, and every other runtime sends no key at all. Before this + * gate the shell's two banners (`ReadRateBanner`, `StorageUsageBanner`) asked + * on every admin page load regardless, so a self-hosted admin logged a 404 per + * shell mount. + * + * What is pinned, against the real shell, the real runtime-config singleton + * (filled through `initRuntimeConfig`, so the payload mapping is on the path) + * and the real shared endpoint reader — only the admin status and the network + * are stood in: + * - an admin on a runtime that sends no `storageUsage` key issues NO request; + * - so does an admin on a runtime that sends `storageUsage: false`; + * - an admin on a runtime that serves it issues ONE request, shared by both + * banners, and the storage banner still renders on a `warn` verdict; + * - a non-admin issues no request even when the runtime serves it. + * + * Every zero below is read after the same settle that sees the one request in + * the served case, so a zero here is a reading, not an unflushed effect. + */ +import '@testing-library/jest-dom/vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor, act } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; + +// Same two stubs the sibling ConsoleShell tests use: sonner is a global toast +// surface, and the ADR-0069 remediation gate needs an AuthProvider above it. +vi.mock('sonner', () => ({ + toast: Object.assign(vi.fn(), { + info: vi.fn(), success: vi.fn(), warning: vi.fn(), error: vi.fn(), + dismiss: vi.fn(), custom: vi.fn(), loading: vi.fn(), promise: vi.fn(), + }), + Toaster: () => null, +})); +vi.mock('../RemediationOverlay', () => ({ RemediationOverlay: () => null })); +vi.mock('@object-ui/auth', async (importOriginal) => ({ + ...(await importOriginal>()), + useWorkspaceAdminStatus: vi.fn(), +})); + +import { useWorkspaceAdminStatus } from '@object-ui/auth'; +import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../runtime-config'; +import { ConsoleShell } from '../ConsoleShell'; + +const asMock = (fn: unknown) => fn as unknown as ReturnType; + +// `createAuthenticatedFetch` reads `response.headers` to adopt a rotated +// session token, so a stub without them is not a Response that lane can use. +function respond(body: unknown, status = 200): Response { + return { + ok: status >= 200 && status < 300, + status, + headers: new Headers(), + json: async () => body, + } as unknown as Response; +} + +/** The endpoint's answer: a `warn` storage verdict and an anomalous read rate. */ +const USAGE = { + state: 'warning', + usedMb: 850, + limitMb: 1024, + warn: true, + blocked: false, + readRate: { state: 'anomalous', readsPerWrite: 700, ratioThreshold: 500 }, +}; + +/** + * Stand in for the network: the runtime config carries `features` as given, + * `/usage/storage` answers {@link USAGE} and is counted, anything else is 404. + */ +function stubNetwork(features: Record): string[] { + const storageRequests: string[] = []; + vi.stubGlobal( + 'fetch', + vi.fn(async (input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; + if (url.endsWith('/api/v1/runtime/config')) return respond({ features }); + if (url.endsWith('/usage/storage')) { + storageRequests.push(url); + return respond(USAGE); + } + return respond({}, 404); + }), + ); + return storageRequests; +} + +function setAdmin(isAdmin: boolean) { + asMock(useWorkspaceAdminStatus).mockReturnValue({ isAdmin, isResolved: true }); +} + +/** Boot the runtime config against `features`, then mount the real shell. */ +async function mountShell(features: Record) { + const storageRequests = stubNetwork(features); + await initRuntimeConfig(); + render( + + +
route
+
+
, + ); + await waitFor(() => expect(screen.getByTestId('route')).toBeInTheDocument()); + // Let every mount effect run and any request it started settle. + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 20)); + }); + return storageRequests; +} + +/** What a runtime that is not the cloud distribution sends: no `storageUsage` key. */ +const SELF_HOSTED = { marketplace: true, installLocal: false, aiStudio: true }; + +describe('ConsoleShell asks for the storage reading only when the runtime serves it (objectui#11002)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + afterEach(() => { + resetRuntimeConfigForTesting(); + vi.unstubAllGlobals(); + }); + + it('an admin on a runtime that sends no storageUsage key issues no request', async () => { + setAdmin(true); + const storageRequests = await mountShell(SELF_HOSTED); + + expect(storageRequests).toHaveLength(0); + expect(screen.queryByTestId('storage-usage-banner')).toBeNull(); + expect(screen.queryByTestId('read-rate-banner')).toBeNull(); + }); + + it('an admin on a runtime that sends storageUsage: false issues no request', async () => { + setAdmin(true); + const storageRequests = await mountShell({ ...SELF_HOSTED, storageUsage: false }); + + expect(storageRequests).toHaveLength(0); + }); + + it('an admin on a runtime that serves it issues ONE request, shared by both banners', async () => { + setAdmin(true); + const storageRequests = await mountShell({ ...SELF_HOSTED, storageUsage: true }); + + await waitFor(() => + expect(screen.getByTestId('storage-usage-banner')).toHaveAttribute('data-storage-usage-case', 'warning'), + ); + expect(screen.getByTestId('read-rate-banner')).toHaveAttribute('data-read-rate-case', 'anomalous-ratio'); + expect(storageRequests).toHaveLength(1); + }); + + it('a non-admin issues no request even when the runtime serves it', async () => { + setAdmin(false); + const storageRequests = await mountShell({ ...SELF_HOSTED, storageUsage: true }); + + expect(storageRequests).toHaveLength(0); + expect(screen.queryByTestId('storage-usage-banner')).toBeNull(); + }); +}); diff --git a/packages/app-shell/src/layout/ReadRateBanner.tsx b/packages/app-shell/src/layout/ReadRateBanner.tsx index da2ac1f669..e858cbb09c 100644 --- a/packages/app-shell/src/layout/ReadRateBanner.tsx +++ b/packages/app-shell/src/layout/ReadRateBanner.tsx @@ -51,13 +51,17 @@ * The audience is the environment's own admin — the only person who can act on * their own app's read pattern. Gated on `useWorkspaceAdminStatus`, which also * keeps the request itself off every ordinary session rather than spending a - * 403 per page load. + * 403 per page load. The request is gated as well on + * {@link isStorageUsageServed} — the runtime's own `features.storageUsage`, + * the one boolean `StorageUsageBanner` reads too — so a runtime that does not + * serve the endpoint is not spent a 404 per page load either (objectui#11002). */ import { TriangleAlert } from 'lucide-react'; import { cn } from '@object-ui/components'; import { useWorkspaceAdminStatus } from '@object-ui/auth'; import { useObjectTranslation, useDisplayLocale, formatDisplayNumber } from '@object-ui/i18n'; import { useReadRateReading, classifyReadRate } from '../hooks/useReadRateReading.js'; +import { isStorageUsageServed } from '../runtime-config.js'; export interface ReadRateBannerProps { /** Override the resolved tenant runtime base (e.g. `/api/v1`). */ @@ -69,7 +73,10 @@ export function ReadRateBanner({ apiBase, className }: ReadRateBannerProps) { const { t } = useObjectTranslation(); const locale = useDisplayLocale(); const { isAdmin } = useWorkspaceAdminStatus(); - const snapshot = useReadRateReading({ apiBase, enabled: isAdmin }); + // Asked only when the runtime serves the endpoint (objectui#11002): the one + // boolean `StorageUsageBanner` gates on too, so neither banner probes a + // runtime that would answer 404. + const snapshot = useReadRateReading({ apiBase, enabled: isAdmin && isStorageUsageServed() }); const bannerCase = classifyReadRate(snapshot); const reading = snapshot.status === 'measured' ? snapshot.reading : null; diff --git a/packages/app-shell/src/layout/StorageUsageBanner.tsx b/packages/app-shell/src/layout/StorageUsageBanner.tsx index 854741bae6..48b27f2375 100644 --- a/packages/app-shell/src/layout/StorageUsageBanner.tsx +++ b/packages/app-shell/src/layout/StorageUsageBanner.tsx @@ -35,6 +35,12 @@ * environment's administrator, gated on `useWorkspaceAdminStatus` — the same * gate the read-rate report uses — which also keeps the request itself off every * ordinary session. Both banners read one response (`readStorageUsage`). + * + * Neither asks a runtime that does not serve that response: the request is + * also gated on {@link isStorageUsageServed}, the runtime's own + * `features.storageUsage`, which only the cloud distribution that mounts the + * endpoint sends (objectui#11002). One boolean for both banners, off by + * default, so a self-hosted admin's page load issues no request at all. */ import { HardDrive } from 'lucide-react'; import { cn } from '@object-ui/components'; @@ -42,6 +48,7 @@ import { useWorkspaceAdminStatus } from '@object-ui/auth'; import { useObjectTranslation, useDisplayLocale, formatDisplayNumber } from '@object-ui/i18n'; import { useStorageUsageReading, classifyStorageUsage } from '../hooks/useStorageUsageReading.js'; import { cloudConsoleUrl } from '../console/marketplace/marketplaceApi.js'; +import { isStorageUsageServed } from '../runtime-config.js'; export interface StorageUsageBannerProps { /** Override the resolved tenant runtime base (e.g. `/api/v1`). */ @@ -53,7 +60,10 @@ export function StorageUsageBanner({ apiBase, className }: StorageUsageBannerPro const { t } = useObjectTranslation(); const locale = useDisplayLocale(); const { isAdmin } = useWorkspaceAdminStatus(); - const snapshot = useStorageUsageReading({ apiBase, enabled: isAdmin }); + // Asked only when the runtime serves the endpoint (objectui#11002): the one + // boolean `ReadRateBanner` gates on too, so neither banner probes a runtime + // that would answer 404. + const snapshot = useStorageUsageReading({ apiBase, enabled: isAdmin && isStorageUsageServed() }); const view = classifyStorageUsage(snapshot); // Below every hook, so hook order is stable as the reading arrives. diff --git a/packages/app-shell/src/runtime-config.test.ts b/packages/app-shell/src/runtime-config.test.ts index 021f0697ce..248108d9d3 100644 --- a/packages/app-shell/src/runtime-config.test.ts +++ b/packages/app-shell/src/runtime-config.test.ts @@ -9,7 +9,7 @@ */ import { describe, it, expect, afterEach, vi } from 'vitest'; -import { initRuntimeConfig, getRuntimeConfig, getPlatformStage, isAiStudioEnabled, isMarketplaceEnabled, getClientErrorReporting, resetRuntimeConfigForTesting } from './runtime-config.js'; +import { initRuntimeConfig, getRuntimeConfig, getPlatformStage, isAiStudioEnabled, isMarketplaceEnabled, isStorageUsageServed, getClientErrorReporting, resetRuntimeConfigForTesting } from './runtime-config.js'; function mockConfig(features: Record) { vi.stubGlobal('fetch', vi.fn(async () => ({ @@ -161,6 +161,63 @@ describe('runtime-config isAiStudioEnabled (objectui#5577)', () => { }); }); +/** + * `features.storageUsage` — does this runtime serve `GET /api/v1/usage/storage`? + * (objectui#11002) + * + * A composition fact, not a plan one: the cloud distribution serves `true` + * exactly when it mounts the endpoint, and every other runtime sends no key. + * The two console banners that read that endpoint ask it only when this is on, + * so it is OFF unless the server sends the literal `true` — the runtimes that + * leave the question unanswered are exactly the ones that would answer the + * request with a 404. `'true'` and `1` are payloads a consumer should not teach + * itself to accept. + */ +describe('runtime-config storageUsage (objectui#11002)', () => { + it('is OFF before init', () => { + resetRuntimeConfigForTesting(); + expect(getRuntimeConfig().features.storageUsage).toBe(false); + expect(isStorageUsageServed()).toBe(false); + }); + + it('is ON when the server sends the literal true', async () => { + mockConfig({ storageUsage: true }); + await initRuntimeConfig(); + expect(getRuntimeConfig().features.storageUsage).toBe(true); + expect(isStorageUsageServed()).toBe(true); + }); + + it.each([ + ['absent (a self-hosted runtime)', { marketplace: true, installLocal: false }], + ['false', { storageUsage: false }], + ["the string 'true'", { storageUsage: 'true' }], + ['the number 1', { storageUsage: 1 }], + ])('is OFF when the key is %s', async (_label, features) => { + mockConfig(features); + await initRuntimeConfig(); + expect(getRuntimeConfig().features.storageUsage).toBe(false); + expect(isStorageUsageServed()).toBe(false); + }); + + it('is OFF when the config fetch itself fails', async () => { + vi.stubGlobal('fetch', vi.fn(async () => { + throw new Error('network down'); + }) as any); + await initRuntimeConfig(); + expect(isStorageUsageServed()).toBe(false); + }); + + it('withdraws it when a later fetch no longer serves it', async () => { + mockConfig({ storageUsage: true }); + await initRuntimeConfig(); + expect(isStorageUsageServed()).toBe(true); + + mockConfig({ marketplace: true }); + await initRuntimeConfig(); + expect(isStorageUsageServed()).toBe(false); + }); +}); + describe('runtime-config platform stage', () => { it('defaults to preview before init (badge shows out of the box)', () => { resetRuntimeConfigForTesting(); diff --git a/packages/app-shell/src/runtime-config.ts b/packages/app-shell/src/runtime-config.ts index 70f1f75477..bc41a2a03e 100644 --- a/packages/app-shell/src/runtime-config.ts +++ b/packages/app-shell/src/runtime-config.ts @@ -78,6 +78,17 @@ export interface RuntimeFeatures { * implied by this declaration). */ scim?: boolean; + /** + * `GET /api/v1/usage/storage` is served by this runtime — the one response + * behind the environment admin's storage-capacity banner and read-rate + * report. Optional cloud flag — absent on self-hosted / vanilla runtimes + * (treated as off). Server-derived from the runtime's COMPOSITION, not from + * the plan: the cloud distribution serves it `true` exactly when it mounts + * the endpoint's plugin (cloud#2481), and no key at all otherwise. When off, + * the console never asks that endpoint (objectui#11002); see + * {@link isStorageUsageServed}. + */ + storageUsage?: boolean; } /** @@ -289,7 +300,7 @@ const defaults: AppShellRuntimeConfig = { singleEnvironment: false, defaultOrgId: null, defaultEnvironmentId: null, - features: { installLocal: false, marketplace: true, aiStudio: true, autoPublishAiBuilds: true, customDomain: false, sso: false, scim: false }, + features: { installLocal: false, marketplace: true, aiStudio: true, autoPublishAiBuilds: true, customDomain: false, sso: false, scim: false, storageUsage: false }, // `stage: 'preview'` while the whole platform is pre-GA, so the badge shows // out of the box on any runtime that hasn't sent an explicit stage yet. branding: { productName: 'ObjectOS', productShortName: 'ObjectOS', stage: 'preview', brandColor: '#4F46E5', pwaThemeColor: '#4f46e5' }, @@ -376,6 +387,10 @@ export async function initRuntimeConfig(baseUrl: string = ''): Promise { customDomain: body.features.customDomain === true, sso: body.features.sso === true, scim: body.features.scim === true, + // A composition fact, not a commercial one, but OFF unless served for + // the same reason: an unknown/older runtime is exactly the one that + // does not serve the endpoint, and asking it is a 404 per page load. + storageUsage: body.features.storageUsage === true, } : current.features, // Read off the RAW body, not off `body.telemetry`: the mirrored reader @@ -559,6 +574,36 @@ export function isAiStudioEnabled(): boolean { return current.features?.aiStudio !== false; } +/** + * Does this runtime serve `GET /api/v1/usage/storage`? (objectui#11002) + * + * Reads the server's OWN answer — `features.storageUsage`, which the cloud + * distribution derives from its composition (cloud#2481): `true` exactly when + * the endpoint is mounted, and no key at all on every other runtime. Both + * console surfaces that read that one response, `StorageUsageBanner` and + * `ReadRateBanner`, gate their request on this one boolean, so a runtime that + * does not serve the endpoint is never asked. + * + * ⛔ Never infer this from the SHAPE OF A FAILURE. "Ask once and remember the + * 404" spends the very request this flag exists to withhold, and reads a + * control plane that is merely down as a runtime without the endpoint. + * + * Fails CLOSED (`=== true`), the opposite direction from + * {@link isMarketplaceEnabled} and {@link isAiStudioEnabled}. Those withhold a + * working capability on an unanswered question; this one decides whether to + * make a request at all, and every unanswered question — a runtime predating + * the key, a self-hosted runtime, a failed config fetch — is precisely the + * population that would answer it with a 404. The cloud runtime that serves + * the endpoint is also the one that serves the flag. + * + * `features?.` for the reason {@link isAiStudioEnabled} gives: a caller reached + * through a partial snapshot sees `features` absent, and that must read as off, + * not throw. + */ +export function isStorageUsageServed(): boolean { + return current.features?.storageUsage === true; +} + /** * The error-reporting sink this deployment's runtime served, or `null` for * "send nothing" (objectui#5522 / objectstack#12681). From 27044c07ee673f0fdc5b0ea1d6bf7725d357b9c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 10:08:21 +0000 Subject: [PATCH 2/2] test(app-shell): drop the needless any cast on the fetch-fails storageUsage 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 --- packages/app-shell/src/runtime-config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-shell/src/runtime-config.test.ts b/packages/app-shell/src/runtime-config.test.ts index 248108d9d3..6c44bc135d 100644 --- a/packages/app-shell/src/runtime-config.test.ts +++ b/packages/app-shell/src/runtime-config.test.ts @@ -202,7 +202,7 @@ describe('runtime-config storageUsage (objectui#11002)', () => { it('is OFF when the config fetch itself fails', async () => { vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('network down'); - }) as any); + })); await initRuntimeConfig(); expect(isStorageUsageServed()).toBe(false); });