Skip to content

[seam -> repo:objectui] the 80% / 100% storage banner has no renderer — cloud serves the numbers and the verdict, nothing shows them #10439

Description

@hotlong

Blocked-by: #10916
Unlock-action: re-check PR #10910

Filed by the repo:cloud execution PM seat (seat post objectstack#6026), session session_01TAUTP6Yky8QWoHUAPDKNJQ, at 2026-09-16T07:58Z, as the objectui half of cloud#2135. ⛔ Not graded or routed here beyond the lane label — that is the objectui seat's.

⚠️ Filed in this repo rather than in objectstack-ai/objectui because that repository is not reachable to this session's credential; this is the established seam carrier for that case (objectstack#18377 is the same shape).

What exists already, and what does not

The per-environment storage cap (产品负责人 adjudication 2026-09-08: Free 1 GB / Team 20 GB per environment; intercept attachment upload and bulk import only) is implemented and enforced on the cloud side:

  • GET /api/v1/usage/storage serves storageUsedMb / storageLimitMb and the same verdict that refuses an upload — served by StorageUsageEndpointPlugin, registered at packages/objectos-runtime/src/objectos-stack.ts:580.
  • An over-limit POST /api/v1/storage/upload/presigned answers 403 STORAGE_LIMIT_REACHED with an upgrade envelope. Proven end to end on the assembled host stack by cloud PR docs(skill): add the first automation eval — ADR-0044 revise loop objectstack#2300, including an ablation: with the guardrail removed the same request answers 200 with a real upload URL and a record is created on a full environment.

What has no renderer is the adjudication's warning half:

提醒:≥ 80% 环境管理员横幅「已用 X / Y」;≥ 100% 横幅升级为「已满,上传与导入已暂停」+ 升级入口。

cloud PR objectstack-ai/objectstack#2143's own landing note records that the banner renders in objectui and was out of scope there, and the cloud#2135 dev found no cloud-side owner for it. ⇒ Today a Free environment crosses 80% with nothing shown, and hits 100% by discovering that an upload fails.

What this card asks

An environment-admin banner driven by GET /api/v1/usage/storage:

  • ≥ 80% — 「已用 X / Y」, informational.
  • ≥ 100% — 「已满,上传与导入已暂停」 plus the upgrade entry point.
  • Visible to environment administrators. ⛔ Not to every member — the adjudication says 环境管理员.

⭐ Read the verdict, do not re-derive the threshold. The endpoint serves the same verdict the guardrail refuses with, precisely so that what the banner shows is what will block an upload. A UI that recomputes used/limit >= 0.8 locally will drift from the enforcement point the first time the rule changes, and the drift will be silent in the direction users notice last.

⛔ Out of scope here

The cloud side. The numbers, the endpoint, the verdict and the refusal are all landed and measured; ⛔ nothing in this card should change them. If the endpoint is missing a field the UI needs, that is a cloud card — say so rather than computing around it.

Provenance

cloud#2135 (the parent, whose runtime half is complete) · cloud#2131 (epic) · cloud PR objectstack-ai/objectstack#2143 (control plane + runtime) · cloud PR objectstack-ai/objectstack#2300 (the end-to-end proof) · docs/design/pricing.md Data volume row.

查重词

storage banner 80% 100% · STORAGE_LIMIT_REACHED banner · GET /api/v1/usage/storage renderer · 环境管理员 容量横幅 · storage quota console banner


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    Migration to objectstack-ai/objectui — waiting on the transfer door · 2026-09-23T14:57Z

    Blocked-by: objectstack-ai/objectstack#19884

    Session session_01X7HwfPLpQtCixDMrRGkSbe, acting on the maintainer instruction 「objectui 你负责迁移。fleet 中继不支持 GitHub 的 issue transfer, 应该立 skills 卡解决」 (2026-09-23).

    Routing rule 1 (cross-repo-coordination): this card's fix lands entirely in objectstack-ai/objectui, and nothing is left on the objectstack side, so it moves there. The fleet relay cannot transfer issues yet: there is no transfer op, and the minted token covers a single repo. The capability is filed as objectstack-ai/objectstack#19884. This card will be transferred rather than rebuilt, so its thread and history travel with it. pm:queue → pm:blocked until then. ⛔ Do not dispatch it here.


    Generated by Claude Code

  2. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Migrated from objectstack-ai/objectstack#18387 — pm:blocked → pm:queue, lane domain:ui · 2026-09-25T01:07Z

    Acting on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「迁移卡 帮我处理」; where — the maintainer's chat with session session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-25. ⛔ Not a claim and not a dispatch.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1
    Session: session_015AUunPkX7UTkCH9e7AdZo1
    Account: os-zhuang (the seat's linked user as GET /user answers it; the maintainer authorised this seat to run on it, verbatim 「允许你用当前账户继续开发」, seat-post marker 5864093687)
    Branch: claude/issue-10439-storage-usage-banner
    Worktree: objectui-issue-10439
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: packages/app-shell/src/hooks/useReadRateReading.ts (or a sibling hook in packages/app-shell/src/hooks/ if the endpoint read is shared), a new banner under packages/app-shell/src/layout/, its mount in packages/app-shell/src/console/ConsoleShell.tsx, packages/app-shell/src/hooks/index.ts, the app-shell README's read-rate section, tests beside each, new console.* rows in packages/i18n/src/locales/*.ts, one .changeset/10439-*.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default judgement tier; dispatch-gates --tier --repo objectstack-ai/objectui on these paths: no path-derived mandate)
    Clause-②: no
    Thread-read: 5824965929
    Serial constraints cleared: no in-flight claim or open PR touches the read-rate hook, ReadRateBanner.tsx or ConsoleShell.tsx (claims on the 14 open lane pm:dispatched cards and the files of all 7 open objectui PRs read at 2026-09-28T05:34Z). Same-package in-flight work is on other files: objectui#8524 (this seat, useAiUsage.ts / AiUsageIndicator.tsx; disjoint), objectui#7483, objectui#10519, objectui#8725, objectui#10887. The i18n locale files are shared with objectui#8524, PR objectui#10896 and objectui#10866: ordinary concurrency, merge main before opening the PR.

    Take order: a maintainer pointer, verbatim 「#8524 和 #10439 需要读 cloud 仓 你现在有权限了」 (the chat of session_015AUunPkX7UTkCH9e7AdZo1, 2026-09-28).

    Clause-② reasoning: ReadRateBanner and useReadRateReading are not re-exported from the @object-ui/app-shell entry (src/index.ts, the . export), and the storage banner takes the same route: mounted by ConsoleShell, no new entry export. So no published surface widens. If the implementation needs a new entry export, this claim flips to yes and the dev reports it. The at-tier contract review is owed before enqueue anyway, on the changeset prose.

    Cloud readings, objectstack-ai/cloud main 96eb092

    The wire shape of GET /api/v1/usage/storage (packages/objectos-runtime/src/storage-usage-endpoint-plugin.ts, quoted):

    export interface StorageUsageResponse {
        state: 'ok' | 'warning' | 'exhausted' | 'unlimited' | 'unknown';
        usedMb?: number;      // absent when never metered
        limitMb?: number;     // 0 = unlimited, absent = unknown
        fraction?: number;    // usedMb / limitMb clamped to [0, 1]; absent when unknown or unlimited
        warn: boolean;        // show the capacity banner (80% and up)
        blocked: boolean;     // uploads and bulk imports are currently refused
        warnFraction: number; // the line `warn` flips at (STORAGE_WARN_FRACTION = 0.8)
        readRate?: ReadRateBannerReading;   // already consumed by objectui's ReadRateBanner
    }
    • Correction to the card body: storageUsedMb / storageLimitMb are the registry-row fields the handler reads. The wire keys are usedMb / limitMb.
    • The verdict is served whole (state, warn, blocked, all from evaluateStorageQuota, the same verdict the upload guardrail refuses with), which is the card's 「read the verdict, do not re-derive the threshold」.
    • Every miss answers 200 with { state: 'unknown', warn: false, blocked: false, warnFraction: 0.8 }.
    • The handler does no role check. The environment-administrator audience is the client's to enforce, the way ReadRateBanner already gates on useWorkspaceAdminStatus.
    • This response carries no upgrade flag. Only the 403 STORAGE_LIMIT_REACHED envelope does (upgrade: true, storage-limit-guardrail-plugin.ts).

    Premise re-measured on objectui main 6a7f24e: git grep -c -E "usedMb|warnFraction|STORAGE_LIMIT_REACHED|limitMb" -- packages apps → 0. Control usage/storage → hits in useReadRateReading.ts (3), ReadRateBanner.tsx (1), the hook's test (3) and the README (1). So objectui already reads this endpoint for the read-rate half, and nothing renders the storage half.

    Delivery order: as on objectui#8524, the tenant runtime image bundles objectui at cloud's .objectui-sha. Against a runtime without this route, the fetch fails soft, the same way the read-rate hook's does today.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
    "issue": 10439,
    "status": "done",
    "branch": "claude/issue-10439-storage-usage-banner",
    "pr": "#10910",
    "session": "session_015AUunPkX7UTkCH9e7AdZo1 (subagent: the parent PM seat id; container CLAUDE_CODE_REMOTE_SESSION_ID cse_015AUunPkX7UTkCH9e7AdZo1)",
    "premise_still_valid": true,
    "head": "efa30fc",
    "summary": "Added the environment-admin storage-capacity banner, driven only by the verdict the tenant runtime serves on GET /api/v1/usage/storage. blocked renders "Storage is full: uploads and imports are paused" with the used/limit MB and an upgrade link to cloudConsoleUrl() (left out when that is empty). warn renders the used/limit MB. ok, unknown, unlimited, non-2xx and off-contract payloads render nothing. classifyStorageUsage never compares usedMb with limitMb or fraction with warnFraction. New pieces: useStorageUsageReading (contract-first parse of the flat storage half) and StorageUsageBanner, mounted in ConsoleShell beside ReadRateBanner behind the same useWorkspaceAdminStatus gate. readStorageUsage is a shared in-flight reader, so both banners cost one request. useReadRateReading now reads through it, with its three-way no-banner distinction and all existing tests unchanged. Also added: five console.storageUsage.* keys in all ten locale packs, a README subsection, and a minor changeset for app-shell and i18n. No package-entry export, so Clause-2: no holds. Claim 5864158793 names this branch. Premises re-measured: wire keys are usedMb/limitMb, and objectui main had zero readers of the storage half (target grep exit 1, control usage/storage 4 files). No cloud field gap: StorageLimitRefusal declares details.upgrade as literal true, and an unlimited plan never reaches blocked, so the upgrade entry needs no plan field. useWorkspaceAdminStatus (org owner/admin membership, admin user.role, isPlatformAdmin rung, positions org_owner/org_admin) is the gate ReadRateBanner documents for the same audience, and cloud defines no narrower tenant-console role.",
    "tests": "At HEAD efa30fc, after merging origin/main twice (9f0c84a, then 7ea8118), all heavy runs through os-verify-lock. turbo run build --filter=@object-ui/app-shell^... --concurrency=2: exit 0, 28/28 tasks. pnpm --filter @object-ui/app-shell type-check: exit 0; --listFiles shows both new test files in tsconfig.test.json. pnpm exec vitest run --reporter=verbose over 28 targets (the 4 touched test files, cloudConsoleUrl-7253, the 22 files that render ConsoleShell, packages/i18n/): exit 0, Test Files 99 passed (99), Tests 1404 passed (1404). Per file: useStorageUsageReading 18/18, StorageUsageBanner 9/9, useReadRateReading 9/9 and ReadRateBanner 10/10 (unchanged), all-locales-key-parity 32/32, entry-locale-shape-7479 8/8, consoleUserPreferenceBudget 3/3. Ablation via objectstack scripts/ablation-replace.mjs in WRAP mode; anchor hit x1 to x0 and the blob changed on disk; afterwards the blob equalled HEAD and git diff HEAD was empty; no dist leg, since the tests import src by relative path. (1) Re-derive from usedMb/limitMb at 0.8: 3 failed / 15 passed (verdict pin, classifier table, and the unlimited plan, where division by a zero limit would raise a banner). (2) Drop the in-flight sharing: 1 failed / 17 passed (the one-request pin). Both went red, as expected beforehand.",
    "gates": [
    {
    "command": "turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 && pnpm --filter @object-ui/app-shell type-check && pnpm exec vitest run (28 targets)",
    "exit": 0,
    "head": "efa30fc"
    },
    {
    "command": "pnpm exec eslint --format json (19 touched .ts/.tsx files)",
    "exit": 0,
    "note": "0 errors, 10 warnings (9 already present at base in the two modified files, 1 new set-state-in-effect matching the read-rate hook), 0 ignored"
    },
    {
    "command": "pnpm check:i18n-keys",
    "exit": 0
    },
    {
    "command": "pnpm check:i18n-drift",
    "exit": 0
    },
    {
    "command": "pnpm check:i18n-dead-keys",
    "exit": 0
    },
    {
    "command": "pnpm check:eager-locale-catalogues",
    "exit": 2,
    "note": "PREREQUISITE NOT MET (needs a built apps/console/dist bundle) -- NOT MEASURED"
    },
    {
    "command": "pnpm check:readme-exports",
    "exit": 1,
    "note": "86 self-imports unjudgeable (unbuilt dist across packages) -- NOT MEASURED; the README diff adds no fenced block and no import binding"
    },
    {
    "command": "pnpm check:vi-mock-specifiers",
    "exit": 0
    },
    {
    "command": "pnpm check:vi-mock-inherit",
    "exit": 0
    },
    {
    "command": "pnpm check:vi-mock-override-shape",
    "exit": 0
    },
    {
    "command": "pnpm check:test-path-roots",
    "exit": 0
    },
    {
    "command": "pnpm check:self-import",
    "exit": 0
    },
    {
    "command": "pnpm check:phantom-deps",
    "exit": 0
    },
    {
    "command": "pnpm check:unreferenced-sources",
    "exit": 0
    },
    {
    "command": "pnpm check:new-line-citations",
    "exit": 0
    },
    {
    "command": "pnpm check:control-bytes",
    "exit": 0
    },
    {
    "command": "pnpm check:changeset-claims",
    "exit": 0
    },
    {
    "command": "pnpm check:pending-changeset-literals",
    "exit": 0
    },
    {
    "command": "node scripts/check-changeset-presence.mjs",
    "exit": 0
    },
    {
    "command": "node scripts/check-changeset-no-major.mjs",
    "exit": 0
    },
    {
    "command": "node scripts/check-governed-queue-guard.mjs --test (21 changed paths)",
    "exit": 0,
    "note": "NOT GOVERNED"
    }
    ],
    "files_changed": [
    ".changeset/10439-storage-usage-banner.md",
    "packages/app-shell/README.md",
    "packages/app-shell/src/console/ConsoleShell.tsx",
    "packages/app-shell/src/hooks/tests/useStorageUsageReading.test.tsx",
    "packages/app-shell/src/hooks/index.ts",
    "packages/app-shell/src/hooks/storageUsageEndpoint.ts",
    "packages/app-shell/src/hooks/useReadRateReading.ts",
    "packages/app-shell/src/hooks/useStorageUsageReading.ts",
    "packages/app-shell/src/layout/StorageUsageBanner.tsx",
    "packages/app-shell/src/layout/tests/StorageUsageBanner.test.tsx",
    "packages/app-shell/src/layout/index.ts",
    "packages/i18n/src/locales/ar.ts",
    "packages/i18n/src/locales/de.ts",
    "packages/i18n/src/locales/en.ts",
    "packages/i18n/src/locales/es.ts",
    "packages/i18n/src/locales/fr.ts",
    "packages/i18n/src/locales/ja.ts",
    "packages/i18n/src/locales/ko.ts",
    "packages/i18n/src/locales/pt.ts",
    "packages/i18n/src/locales/ru.ts",
    "packages/i18n/src/locales/zh.ts"
    ],
    "deviations": [
    "Lint narrowed and declared. Package-scoped lint (pnpm --filter app-shell --filter i18n lint) hit queue-timeout (exit 99) twice while sibling objectui#8524 held the lock about 21 min for a full app-shell vitest run (pid 12765, alive, about 225% CPU). Ran eslint on the 19 touched ts/tsx files instead. Evidence: (1) the population is ESLint own --format json output, 19 files, 0 ignored; (2) the count is read from that JSON; (3) root eslint.config.js sets no parserOptions.project or projectService, loads no import-resolving plugin, and no local rule reads the disk, so no untouched file verdict can move.",
    "check:eager-locale-catalogues NOT MEASURED (needs a built console bundle). The diff only adds rows inside existing catalogue literals and touches no import, locales/index.ts or registry.ts. The source-shape pin entry-locale-shape-7479 passed 8/8.",
    "check:readme-exports NOT MEASURED (exit 1 from unjudgeable self-imports across unbuilt packages). The README diff adds no fenced block or import binding.",
    "The file surface also includes packages/app-shell/src/hooks/storageUsageEndpoint.ts and packages/app-shell/src/layout/index.ts. Both are covered by the claim: "a sibling hook in hooks/ if the endpoint read is shared" and "a new banner under layout/".",
    "resolveRuntimeApiBase moved into the shared reader and is re-exported from useReadRateReading.ts so the barrel and the existing tests stay unchanged.",
    "The PR body footer uses the os-dev contract form (Generated by ... session URL), not the harness reminder form. The os-dev file says harness attribution reminders yield to it."
    ],
    "mcp_calls": "0 -- no MCP GitHub tool was called",
    "api_writes": "3 REST writes, all through the fleet relay (each is one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create POST /repos/objectstack-ai/objectui/pulls (draft #10910, relay run 36387948884); assign via label-write.mjs on /repos//issues/10910 (os-zhuang, relay run 36388017546, read-back MATCHES); comment POST /repos//issues/10439/comments (this os-dev-report). Also 5 git pushes to the branch (not REST). REST reads: issue 10439 and its comments, and the pulls list by head.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: exception: release-text · evidence: pending .changeset/9954-read-rate-banner.md says "useReadRateReading ... and ReadRateBanner ... Both are exported from @object-ui/app-shell". The package entry (packages/app-shell/src/index.ts, the only exports target) names neither: git grep -c -E "ReadRate" on that file finds nothing (exit 1); control: git grep -c ReadRateBanner on src/layout/index.ts finds 1. The text publishes verbatim into the CHANGELOG at release. carrier: whoever next edits that changeset (none claimed); for the seat to file or edit. · dedupe words: 9954 changeset exported claim, ReadRateBanner not exported, read-rate changeset package entry",
    "carrier: 承接者:无 · noted, not filed: no content/docs/guide page covers the console chrome banners (read-rate, impersonation, now storage); the app-shell README is the only doc surface (AGENTS.md #2 asks for both). In PR Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed: cloudConsoleUrl() lands on the control plane origin, not a billing or upgrade page. That is the standing objectui#7253 limit (no control-plane-declared upgrade URL for tenant SPAs). In PR Acceptance notes."
    ]
    }

  5. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    ACCEPT — PR #10910 @ efa30fc

    domain:ui seat 4, session_015AUunPkX7UTkCH9e7AdZo1, at 2026-09-28T07:17Z. Checked against GitHub, origin/main and cloud main, not against the report.

    • Form: draft, base main, body first line Fixes #10439, and no other closing keyword in the body. 21 files, +989/−33, all inside the claim's file surface (the new storageUsageEndpoint.ts is the shared reader the claim allowed; layout/index.ts gains one sub-barrel line). Nothing under content/docs/releases/. Changeset .changeset/10439-storage-usage-banner.md: @object-ui/app-shell minor and @object-ui/i18n minor, no major.
    • CI on the head efa30fcfe8: 43 check-runs, 40 success and 3 skipped (conditional jobs), none failed. mergeable_state: clean. Governed-surface guard (scripts/check-governed-queue-guard.mjs --test, 21 paths): NOT GOVERNED. Size is well under 5000 lines.
    • Contract review: PASS at CONTRACT_REVIEW_TIER on this head, 5865258297. It verified Clause-②: no at the head itself: the package entry has no wildcard re-export and names none of the new symbols, and exports is . and ./styles.css only.
    • Spot readings (seat's own): classifyStorageUsage reads the served warn / blocked only, never fraction or warnFraction, which honours the card's 「read the verdict, do not re-derive the threshold」. The parse is contract-strict on the wire keys usedMb / limitMb. The banner is gated on useWorkspaceAdminStatus (enabled: isAdmin), so a non-admin session issues no request. The upgrade link appears only at blocked. The zh copy 「已用 X MB / Y MB」 and 「存储已满,上传与导入已暂停」 matches the adjudication text. The blocked body says reading, exporting and single-record edits still work; the seat checked cloud's storage-limit-guardrail-plugin.ts, which refuses only bulk import and the attachment-upload family, so that is true. The read-rate hook now shares one in-flight request and keeps its three-way no-banner distinction; its tests are unchanged and green.
    • Findings, one line each:
    • Downstream: as with objectui#8524, the storage banner reaches cloud environments only when cloud's .objectui-sha moves past this merge. That rides on the existing cloud pin card objectstack-ai/cloud#2441; a pointer is posted there.

    Next: ready → auto-merge (merge queue), followed to MERGED by this seat.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Parked on an external gate: pm:dispatched → pm:blocked, work complete

    domain:ui seat 4, session_015AUunPkX7UTkCH9e7AdZo1, at 2026-09-28T07:45Z. The claim and the assignee stand: this seat still owns the landing.


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Unblocked: pm:blocked → pm:dispatched, PR re-queued

    domain:ui seat 4, session_015AUunPkX7UTkCH9e7AdZo1, at 2026-09-28T08:50Z.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions