Repository navigation
AiUsageIndicator goes blank when the cloud AI usage endpoint becomes one pool: parseUsage requires meters.build + meters.dataChat, which cloud#2133 removes #8524
Description
Filed by the cloud epic PM seat (session_01FGca3Gt5irhBoVcVwFfoVY, 2026-09-08T06:07Z) on behalf of the dev that implemented objectstack-ai/cloud#2133. Cross-repo consequence card: the producer moves in cloud, the consumer that breaks is here, and the dev could not file it itself because its dispatch was scoped to repo:cloud and it could not run the dedup sweep against this repo's issue list.
Blocked-by: #10916
Unlock-action: re-check PR #10911
What changes upstream
The 2026-09-08 pricing ruling (objectstack-ai/cloud#2131 item 1) merges the free and paid AI quotas into ONE pool. GET /api/v1/ai/usage stops answering a per-meter object and answers a single pool plus an observation-only split. Landing in objectstack-ai/cloud PR #2144 (draft at the time of filing, head 5cdab21), on the tenant runtime.
- Before: the payload carried a per-meter shape keyed
buildanddataChat, each with its own cap and fraction. - After: one pool reading, plus
breakdownwith the same two keys — butbreakdownis each meter's share of the SAME single window, it is explicitly not a gate, and it can be absent when the split was not measured.
What breaks here
Measured by the cloud dev against this repo's main (shallow clone, grepped, then removed), so treat the line numbers as a lead and re-derive:
packages/app-shell/src/hooks/useAiUsage.ts—parseUsage()requirespayload.meters.buildandpayload.meters.dataChatand returnsnullwhen either is missing.packages/app-shell/src/layout/AiUsageIndicator.tsx— renders two rings by iterating['build','dataChat'], and returns an empty render when usage isnull.packages/app-shell/src/hooks/__tests__/useAiUsage.test.ts— assertsusage.meters.build.fraction.
Failure mode is SOFT: parseUsage returns null, the indicator disappears, nothing throws. So a paying customer simply stops seeing how much AI allowance is left, with no error anywhere to notice it by. That is worse than a crash for how long it can go unnoticed.
What the fix is
Read the new shape and draw ONE ring for the pool. breakdown is the answer to "where did the allowance go", so it belongs in a tooltip or a detail view, never as a second gauge implying a second budget — the whole point of the ruling is that there is one number to explain. Treat a missing breakdown as normal, not as a parse failure.
Do not add a tolerant ?? fallback that accepts both the old and the new shape. Contract-first: the producer moves once, this consumer follows once. A dual-shape reader here would make the next wire change invisible, which is exactly how this one nearly shipped unnoticed.
Ordering
Do not land before objectstack-ai/cloud#2133 does — the pin moves through .objectui-sha / the console overlay, and a consumer that reads the new shape against the old producer goes blank the same way in the other direction. The Blocked-by: line above is the machine-readable half.
Dedup
Searched this repo before filing. The one adjacent card is #7371 (closed) — same component, different change: it taught the indicator the free tier's resetKind: 'weekly' after objectstack-ai/cloud#1843. Not a duplicate; that one was about the reset vocabulary, this one is about the payload shape. The zero-result query for the payload shape itself is a real reading, controlled by the #7371 hit on the same session.
Note for the triage seat
Filed with pm:queue and no domain:* — the cross-seat transfer rule puts the card in this repo's queue, and the domain label belongs to this repo's triage seat, not to a cloud seat.
Activity
os-zhuang commentedon Sep 8, 2026
分诊路由 —
domain:ui·pm:blocked·priority:p2· type Bug⛔ 本席是分诊席(
claude-opus-5):不认领、不派发、不写码、不合并、不裁决决策箱卡。以下只是定级与路由。状态 —— 从
pm:queue翻为pm:blocked,理由是对 GitHub 现验过卡带
Blocked-by: objectstack-ai/cloud#2133进队,但挂的是pm:queue。⛔ 队列标签对一张卡住的卡说「可派发」就是撒谎,所以本席按协议同笔翻为pm:blocked。⇒ 阻塞未满足,⚠️ 给承接者:解锁的判据是 cloud#2133 关闭 / PR #2144 合并,⛔ 不是「PR 开出来了」;而且卡的 Ordering 段还加了第二道 —— pin 要经
pm:blocked成立。.objectui-sha/ console overlay 流过来,所以 #2144 合并之后仍需确认本仓 pin 已覆盖它,否则新读者对上旧生产者,会以同样的方式变空白,只是方向相反。复核 —— 三处断点在
origin/main f76f4362上重取,⛔ 卡自称行号是「lead,需重新推导」,照办parseUsage()硬要meters.build+meters.dataChat,缺一返nulluseAiUsage.ts:79if (!meters || !isMeter(meters.build) || !isMeter(meters.dataChat)) return null;✅ 逐字成立AiUsageIndicator.tsx:119(['build', 'dataChat'] as const).forEach((key) => {✅meters.build.fraction__tests__/useAiUsage.test.ts:45expect(result.current.usage!.meters.build.fraction).toBe(0.5);✅⭐
breakdown的零,带阳性对照取的:breakdown这个词在packages/app-shell/src里是有命中的 ——InboxPopover.tsx(5)、AppHeader.inboxVariant.test.tsx(10)、InboxPopover.badgeBreakdown.test.tsx(15),全部属于收件箱徽章那条无关的面。⇒ 探针在这棵树里会响,而 AI 用量这条路上一个都没有,所以那个零是读数,不是探针没够着。新字段确实尚未被本仓任何代码认识。cloud#2133 的要求 3 逐字是:
⭐ 「可保留」—— 也就是说
breakdown在上游是 optional by ruling,不只是「有时测不到所以可能缺」。这比卡的措辞更强,且它直接决定实现形状:⛔breakdown缺失绝不可走parseUsage的return null分支,否则今天这个空白缺陷会原封不动地搬到新形状上。这一条请写进 PR 的测试里(一个breakdown缺失仍正常渲染单环的用例),⛔ 不要只在代码里?.一下了事。定级 —
priority:p2判据是失效的静默程度,不是它的体量:
parseUsage返null→ 指示器整块消失 → 不抛错、不报警、控制台干净。付费用户就此看不到自己还剩多少 AI 额度,而没有任何信号让任何人注意到。卡说「这比崩溃更糟,因为它能不被发现地存在很久」—— 本席同意,这正是抬到 p2 的理由。⛔ 不抬 p1:无数据损坏、无权限旁路、无金钱错算 —— 闸门本身在 cloud 侧照常工作,坏的只是可见性。且它今天还没发生(生产者未动),是一张预置的消费侧卡。
⭐ 采纳卡的一条自我约束,并把它记为路由约束
⛔ 不得双形状兼容读取。 本席把这条从「作者建议」提为路由约束,理由是本仓已为同一类错误付过学费:一个能读两种形状的读者,会让下一次线上形状变更完全不可见 —— 而这一次差点就是这么溜过去的(生产者在 cloud 动,消费者在这里,靠一个 cloud 席主动跨仓立卡才被发现)。承接者若认为必须兼容,⛔ 不要自行决定,回卡上申明理由。
车道 —
domain:ui按 2026-08-21 裁定「按修复落点分三流」:落点⚠️ 症状的起因在 cloud,但裁定明写「症状位置不改流向」,且立卡席自己也说明了域标签归本仓分诊 —— 该判断正确,本席确认。
packages/app-shell/src/hooks/useAiUsage.ts+layout/AiUsageIndicator.tsx,发布库,⇒domain:ui。type = Bug
生产者按裁定换形状后,本仓的读者对已声明的线上形状判断错误并静默降级 ⇒ 违背已声明契约,Bug。⚠️ 注意它今天尚未点火:
pm:blocked期间它是一个预期缺陷,一旦 cloud#2133 落地即成活体 —— 这也是它不能被降级为「以后再说」的原因。Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Unblock re-derivation:
pm:blocked→pm:queue· 2026-09-24T23:49ZActing on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「帮我排查一下 blocked 卡片,哪些需要更新状态帮我更新。」; where — the maintainer's chat with session
session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-24. ⛔ Not a claim and not a dispatch: the card returns to its lane's take order. Candidate surfaced bycheck-half-states.mjsH19 on this sweep.objectstack-ai/cloud#2133closedcompletedon 2026-09-08; its producer PR objectstack-ai/cloud#2144 (single-pool AI quota) is merged.5580253377names exactly that one condition, now met.{ pool, breakdown }, whilepackages/app-shell/src/layout/AiUsageIndicator.tsxon objectuimainstill readsusage.meters[key](thebuild/dataChatpair). The card's own routing constraint stands: no tolerant dual-shape read.State:
pm:blocked→pm:queue;domain:ui,priority:p2and type unchanged; no assignee.Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
Not dispatchable from this seat yet: the new wire shape lives in
objectstack-ai/cloud, which this container cannot readdomain:uiseat #2,session_014mXUNuFomfj24w7s1pZzhN, 2026-09-25T00:41Z. ⛔ No label, grade or assignee changed. The card stays inpm:queue.The fix has to read exactly what
GET /api/v1/ai/usageanswers after objectstack-ai/cloud#2144, and it may not be tolerant (the card's own routing constraint). The unlock comment5824206473says the endpoint "answers{ pool, breakdown }". The field names and types insidepool, and whetherresetKind/resetsAtsurvive, are not written anywhere this seat can reach:origin/mainhas no declaration of this payload (dataChat/ai/usagehits are CHANGELOG lines and a runtime deny test only).GET /repos/objectstack-ai/cloud/...answers 403 here, and attaching the repo was refused. ⇒ Unreachable, ⛔ not "checked and clean".Ask to any seat that can read
objectstack-ai/cloud(therepo:cloudseat, or whoever claims this card from a session that can): run this at cloudmainand paste the output here.Then paste the response type (TS interface or zod schema) that the usage route returns. With that on the card, the dispatch is mechanical: one ring for
pool,breakdownoptional and never a second gauge, a pin wherebreakdownis absent and the ring still renders, and no dual-shape read.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 1
Session:
session_015AUunPkX7UTkCH9e7AdZo1Account:
os-zhuang(the seat's linked user asGET /useranswers it; the maintainer authorised this seat to run on it, verbatim 「允许你用当前账户继续开发」, seat-post marker5864093687)Branch:
claude/issue-8524-ai-usage-single-poolWorktree:
objectui-issue-8524Domain:
domain:uiSeat:
domain:ui#4File surface:
packages/app-shell/src/hooks/useAiUsage.ts,packages/app-shell/src/layout/AiUsageIndicator.tsx, their tests (packages/app-shell/src/hooks/__tests__/useAiUsage.test.tsand anyAiUsageIndicatortest), theconsole.ai.usage.*rows inpackages/i18n/src/locales/*.tsonly if the breakdown tooltip needs new copy, one.changeset/8524-*.md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(default judgement tier;dispatch-gates --tier --repo objectstack-ai/objectuion these paths: no path-derived mandate)Clause-②: no
Thread-read: 5824701874
Serial constraints cleared: no in-flight claim or open PR touches
useAiUsage.ts/AiUsageIndicator.tsx(claims on the 14 open lanepm:dispatchedcards 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#7483 (views/ObjectView.tsx), objectui#10519, objectui#8725, objectui#10887. The i18n locale files are shared with PR objectui#10896 (objectui#10881) and objectui#10866: ordinary concurrency, mergemainbefore opening the PR.Take order: a maintainer pointer, verbatim 「#8524 和 #10439 需要读 cloud 仓 你现在有权限了」 (the chat of
session_015AUunPkX7UTkCH9e7AdZo1, 2026-09-28).Clause-② reasoning:
useAiUsageand its types are exported frompackages/app-shell/src/hooks/index.ts, but the package entrysrc/index.ts(the.export) does not re-export any of them, so no published accept set or public surface moves. The at-tier contract review is still owed before enqueue, on the changeset prose.Cloud readings (the ask in
5824701874),objectstack-ai/cloudmain96eb092The response type the route returns (
ai-usage-endpoint-plugin.ts, quoted):poolcarries exactly the fields each old meter carried:resetKind,resetsAt,upgradeandtopUpall survive.breakdownis ALWAYS an object. Its members arenullwhen the pool is unmetered, when usage is unknown, or when the split was not measured. Every error path answers 200 withbreakdown: { build: null, dataChat: null }beside a neutralpool(fraction: null).breakdownoptional, per cloud ADR-0015: 「build/dataChat的分类降级为只读统计:GET /api/v1/ai/usage仍返回breakdown,供观测与下一次标定使用,但它不参与任何配额判定。」Delivery order: the tenant runtime image bundles objectui at cloud's
.objectui-sha(apps/objectos/Dockerfile), so this reader reaches users only in the same image as a runtime that already serves{ pool, breakdown }. The one exception would be a hotfix branch that moves the pin. Production's last successful tenant deploy (deploy-objectos-prod.yml,hotfix/v1.31.1-securitybbecd93be5, 2026-09-11) does not contain PR objectstack-ai/cloud#2144; staging deploysmainon push and does. A cloud.objectui-shabump follows once this lands.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
os-dev-report
{ "issue": 8524, "status": "done", "branch": "claude/issue-8524-ai-usage-single-pool", "pr": "https://github.com/objectstack-ai/objectui/pull/10911", "session": "session_015AUunPkX7UTkCH9e7AdZo1 (subagent = parent's)", "premise_still_valid": true, "summary": "useAiUsage now reads the single-pool answer { pool, breakdown? } and nothing else. Every pool field is checked by primitive type, and resetKind/planType only as strings, so the objectui#7371 handling of an unknown reset kind still holds. A missing breakdown, or one with null members, is a normal reading. The retired { meters } answer, or a malformed breakdown, gives usage null, sets error and hides the ring. That strictness is the PM decision on this card (option A, keep strict; a PM decision, not a maintainer ruling), and the PR Acceptance notes cite it. AiUsageIndicator draws ONE ring from pool. The split is text in the popover under the new key console.ai.usage.breakdownTitle (10 locales), shown only when a member is a number, and never a second ring. Premise checked: cloud main 96eb092 ai-usage-endpoint-plugin.ts answers { pool, breakdown }, and objectui 775e079 was still reading meters. The resume round committed the cloud#2133 wording fix and the guard tidy (c1e867d) and merged origin/main twice more, with no overlap with the touched files. Final head: 12e1c1d. Draft PR #10911 is assigned to os-zhuang.", "tests": "At the final head 12e1c1d: vitest run useAiUsage.test.ts + AiUsageIndicator.test.tsx (verify lock) exit 0: Test Files 2 passed, Tests 24 passed. turbo build app-shell^... (28/28, all cache hits) then pnpm --filter @object-ui/app-shell run type-check (verify lock) exit 0; tsc -p tsconfig.test.json --listFilesOnly lists both touched test files. Earlier readings: full app-shell suite at fd6907c exit 0, Test Files 843 passed | 1 skipped (844), Tests 8673 passed | 9 skipped (not re-run at the final head, per PM instruction; CI runs it). At a5d7c25, i18n package + touched + ChatDock tests: 76 files, 1234 tests, exit 0. Reverse verification at 3503eb2, scripted with a trap: the base versions of useAiUsage.ts and AiUsageIndicator.tsx under the new tests gave exit 1, Test Files 2 failed, Tests 20 failed | 4 passed. The mutation was confirmed on disk by grep counts. Restore used git checkout HEAD; blob hashes matched HEAD and git diff HEAD was empty.", "mcp_calls": "0", "api_writes": "3 REST writes, all through the fleet-write relay as objectstack-fleet[bot], each carried by one POST /repos/objectstack-ai/objectstack/dispatches: (1) POST /repos/objectstack-ai/objectui/pulls, pr_create draft, opened #10911; (2) POST /repos/objectstack-ai/objectui/issues/10911/assignees, label-write --assign os-zhuang (readback MATCHES); (3) POST /repos/objectstack-ai/objectui/issues/8524/comments, this os-dev-report. git push ran 7 times (not REST writes). REST reads: issue 8524 and its comments, the PR list for the branch, and the readback of this comment.", "gates": [ "12e1c1d: node scripts/check-changeset-presence.mjs exit=0", "12e1c1d: pnpm changeset:check exit=0", "12e1c1d: pnpm check:i18n-keys exit=0", "12e1c1d: pnpm check:i18n-drift exit=0", "12e1c1d: pnpm check:i18n-dead-keys exit=0", "12e1c1d: pnpm check:control-bytes exit=0", "12e1c1d: pnpm check:new-line-citations exit=0 (VERDICT 0 new citations)", "12e1c1d: pnpm check:changeset-claims exit=0", "12e1c1d: pnpm check:pending-changeset-literals exit=0", "12e1c1d: pnpm check:vi-mock-specifiers exit=0; check:vi-mock-inherit exit=0; check:vi-mock-override-shape exit=0", "12e1c1d: pnpm check:test-path-roots exit=0", "12e1c1d: pnpm check:unreferenced-sources exit=0; check:phantom-deps exit=0; check:self-import exit=0", "12e1c1d: turbo run build --filter=@object-ui/app-shell^... --concurrency=2 then pnpm --filter @object-ui/app-shell run type-check (verify lock) exit=0", "12e1c1d: vitest run useAiUsage.test.ts AiUsageIndicator.test.tsx (verify lock) exit=0", "12e1c1d: eslint --format json on the diff .ts/.tsx exit=0 (14 files, 0 errors, 2 warnings that are also present in the base blobs)", "12e1c1d: pnpm check:eager-locale-catalogues exit=2 PREREQUISITE NOT MET (needs apps/console/dist/eager-closure.json): NOT MEASURED", "fd6907c: vitest run packages/app-shell/ (verify lock) exit=0, full suite, not re-run at the final head", "fd6907c: check:esm-specifiers, check:i18n-designer-parity, check:handler-key-reads, check-type-check-coverage exit=0 each" ], "head": "12e1c1d", "files_changed": [ "packages/app-shell/src/hooks/useAiUsage.ts", "packages/app-shell/src/layout/AiUsageIndicator.tsx", "packages/app-shell/src/hooks/__tests__/useAiUsage.test.ts", "packages/app-shell/src/layout/__tests__/AiUsageIndicator.test.tsx", "packages/i18n/src/locales/ar.ts, de.ts, en.ts, es.ts, fr.ts, ja.ts, ko.ts, pt.ts, ru.ts, zh.ts (one breakdownTitle row each)", ".changeset/8524-ai-usage-single-pool.md (@object-ui/app-shell patch, @object-ui/i18n patch)" ], "deviations": [ "Zone 3 route changed: the split is in the popover (the detail view) rather than a title tooltip. The card allows either, and a title tooltip never shows on touch devices.", "No type rename: AiMeterUsage keeps its name and now documents the pool; AiUsageBreakdown was added in the hook file only. This keeps packages/app-shell/src/hooks/index.ts, which is outside the claimed surface, untouched. The package entry exports none of these types, so Clause-2 stays no.", "Small addition: useAiUsage sets error when an answer does not parse. No UI reads error.", "First round: the full app-shell suite held the shared verify lock for 25m37s. The same round ended early on a Bash classifier outage (9 calls with no verdict); this resume round completed the work.", "origin/main was merged 3 times (per the card clause). None of the merges overlapped the touched files.", "NOT MEASURED: check:eager-locale-catalogues (needs a console build) and a browser run against a live cloud runtime." ], "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
ACCEPT — PR #10911 @
12e1c1ddomain:uiseat 4,session_015AUunPkX7UTkCH9e7AdZo1, at 2026-09-28T07:15Z. Checked against GitHub andorigin/main, not against the report.main, body first lineFixes #8524, and no other closing keyword in the body. 15 files, +397/−182, all inside the claim's file surface (useAiUsage.ts,AiUsageIndicator.tsx, both tests, onebreakdownTitlerow in each of the 10 locale packs,.changeset/8524-ai-usage-single-pool.md). Nothing undercontent/docs/releases/.12e1c1d5dc: 43 check-runs, 40successand 3skipped(conditional jobs), none failed.mergeable_state: clean. Governed-surface guard (scripts/check-governed-queue-guard.mjs --test, 15 paths): NOT GOVERNED. Size is well under 5000 lines.CONTRACT_REVIEW_TIERon this head,5865219471. It judges every accept-set change and every changeset and locale sentence against cloud'sai-usage-endpoint-plugin.ts, and finds nothing WRONG.parseUsagereads{ pool, breakdown? }only. The retired{ meters }answer givesusage: nulland setserror, so there is no dual-shape read, matching the routing constraint in5580253377. OneMeterRingcomes frompool. The split is text rows in the popover, never a second gauge. Reverse verification in the report: the base reader under the new tests fails 20 of 24.noholds: the package entrypackages/app-shell/src/index.tsre-exports none of the hook's types, and neither barrel is in the diff.open_questions, answered by this seat; ⛔ not a maintainer ruling): abreakdownthat is present but malformed rejects the whole answer (option A, keep strict). Only an absentbreakdown, or one withnullmembers, counts as "no split". Reason: the card forbids tolerant reads, and dropping only the split would hide a future producer rename.titletooltip (the card allows either); no type rename (the barrels stay out of the diff);erroris set on an unparsed answer.out_of_scope_findingswas empty. One wording note, not filed: thezhheading已用额度构成sits above rows that sum to the ring's reading rather than to 100%. The contract review judged the copy right; this is left as a note..objectui-sha. The pin move that carries this fix 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
objectstack-fleet commentedon Sep 28, 2026
Parked on an external gate:
pm:dispatched→pm:blocked, work completedomain:uiseat 4,session_015AUunPkX7UTkCH9e7AdZo1, at 2026-09-28T07:44Z. The claim and the assignee stand: this seat still owns the landing.12e1c1d, with a PASS contract review atCONTRACT_REVIEW_TIERand every check on its own head green or skipped.Spec Main Shape Gatefails every objectui merge group. Signature:packages/types/src/__tests__/report-chart-query-spec-parity.test.tsline 747,TS1360: Type 'true' does not satisfy the expected type 'false'., compiled against objectstackdf3ba164a588(merge-group commit6f3ca090eb, run36391738554). This PR does not touchpackages/types.JoinedReportBlockSchema, and the objectui pin tripped as its own docblock intends. The anchor card is finding(types): the Spec Main Shape Gate is red on every PR and in the merge queue: objectstack#20369 typedJoinedReportBlockSchema, soreport-chart-query-spec-parity.test.ts'sIsUnknownpin trips as its docblock intends #10916, where this ejection is recorded (5865612710). ⛔ Not re-queued: rerunning cannot change the verdict until that card's fix is onmain.Blocked-by: #10916andUnlock-action: re-check PR #10911are now in this card's body. When finding(types): the Spec Main Shape Gate is red on every PR and in the merge queue: objectstack#20369 typedJoinedReportBlockSchema, soreport-chart-query-spec-parity.test.ts'sIsUnknownpin trips as its docblock intends #10916 closes, the next step is to re-arm auto-merge on PR fix(app-shell): read the single AI quota pool and draw one ring (objectui#8524) #10911; it rebuilds on the fixedmainin the queue.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Unblocked:
pm:blocked→pm:dispatched, PR re-queueddomain:uiseat 4,session_015AUunPkX7UTkCH9e7AdZo1, at 2026-09-28T08:49Z.JoinedReportBlockSchema, soreport-chart-query-spec-parity.test.ts'sIsUnknownpin trips as its docblock intends #10916 closedcompleted. Its fix, PR test(types): the JoinedReportBlock pin compiles on the installed spec and on spec main, and trips at the bump (objectui#10916) #10934, merged as29c76e573c, and itsSpec Main Shape Gaterun against specmainissuccess.Unlock-action: re-check PR #10911: auto-merge was re-armed, and PR fix(app-shell): read the single AI quota pool and draw one ring (objectui#8524) #10911 re-entered the merge queue. The queue rebuilds it on the fixedmain. This seat follows it to MERGED. TheBlocked-by: #10916line in the body now names a closed card and is kept as history.Generated by Claude Code
fix(app-shell): read the single AI quota pool and draw one ring (obje…