Skip to content

feat(platform-objects,service-messaging): Setup lists show their own empty state; API Keys gains Create Key - #23924

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23852-setup-empty-states
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23852-setup-empty-states

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23852

Clause-②: no

Setup's Invitations, API Keys, OAuth Applications and the three notification lists now draw their own empty state instead of the console's fallbacks, and API Keys gains a Create Key button that mints through the existing POST /api/v1/keys route. Metadata, bundles and tests only; no field, index, permission or route changes.

What changes

1. Every list view declares its own emptyState (title + one sentence).

object view title
sys_invitation pending (Setup opens this one) No pending invitations
accepted No accepted invitations
expired No expired or canceled invitations
all_invitations No invitations yet
sys_api_key all_keys (Setup) No API keys yet
mine (Account app) You have no API keys yet
active No active API keys
revoked No revoked keys
sys_oauth_application all_apps (Setup) No OAuth applications yet
mine (Account app) You have no OAuth applications yet
active No active OAuth applications
disabled_apps No disabled OAuth applications
sys_notification_preference all (new) No preferences yet
sys_notification_subscription all (new) No subscriptions yet
sys_notification_template all (new) No templates yet

The three identity objects are managedBy: 'better-auth', so with no declaration objectui drew the identity fallback ("created by the authentication provider … not added by hand here"), which is false for all three.

The three notification objects had no list view at all (a PM mechanism assumption this falsifies: the claim says "the list views of" them, and there were none). The only per-list empty copy the spec carries is a list view's emptyState, so each now ships one view, all, declared for that copy alone. It has columns: [], which is the spec's own "declares no projection, left to the renderer" form, so objectui keeps deriving the same columns from highlightFields (org attribution column included), and it has no filter, sort or pagination. The view id all is the id objectui's synthesized tab already had. The one visible side effect is the tab label: "All" (translated) instead of the console's "All records".

2. API Keys: create_api_key ("Create Key"), a primary list_toolbar action. It calls POST /api/v1/keys with a required name (field-backed on sys_api_key.name) and shows the raw key in a resultDialog (path: 'key', format: 'secret'). It declares no successMessage, because the dialog suppresses the toast. It appears on every sys_api_key list, Setup's all_keys and the Account app's mine. The route pins the key to the caller (user_id and the active organization are never read from the body), so an admin on Setup mints their own key exactly as a user does on My Keys. The param dialog's description says this.

3. Copy in every locale. The en bundles are regenerated with node scripts/check-i18n-bundles.mjs --write, and the zh-CN, ja-JP and es-ES leaves are hand-written. Rerunning --write after the translation dropped every provenance row the fill had added, so the *.source-hashes.generated.ts files have no net change.

Item 2's route reading (the card's condition)

The card asks the claimant to confirm the route and, if a new door is needed, to treat that half as its own question. No new door is needed:

  • The route is POST /api/v1/keys: packages/runtime/src/domains/keys.ts (createKeysDomains / handleKeysRequest), mounted at server.post(prefix + '/keys') in packages/runtime/src/dispatcher-plugin.ts, route-ledger row POST /keys (disposition: 'sdk', client: 'keys.create'). It is the only mint path. It whitelists name and an optional expires_at, answers 201 { success, data: { id, name, prefix, key, … } } with the raw key once, and stores only the hash. It does not depend on sys_api_key's create affordance, which stays off. The PM's grep for createApiKey / api-keys missed it because the names are keys / createKeysDomains.
  • Its console callers today (objectui origin/main a5bb7ac) are the mcp:connect-agent widget (packages/app-shell/src/console/connect/ConnectAgentWidget.tsx) and the developer Integrations page (apps/console/src/pages/developer/AgentConnectSection.tsx). The Account app's API Keys entry (nav_account_api_keys → sys_api_key view mine) rendered the same object list as Setup, with no create entry. This action is now that entry on both pages.
  • An object action reaches it unchanged. objectui's apiHandler (useConsoleActionRuntime.tsx) POSTs an absolute type: 'api' target with the session cookie, unwraps { success, data }, and resolves resultDialog paths against data. That is the mechanism sys_oauth_application.create_oauth_application already uses for its one-shot client secret.

Tests (metadata level)

  • packages/platform-objects/src/identity/setup-list-empty-states.test.ts (new). For the three identity objects, the population is each object's declared views. It checks that every view declares a non-empty title and message, that no two views of one object share a title (a filtered view names itself), and that Pending's title is "No pending invitations" (the card's acceptance names it). It checks that the en bundle carries each view's copy as declared and that zh-CN / ja-JP / es-ES carry it. For Create Key, it checks the primary list_toolbar placement, POST /api/v1/keys, the key reveal as secret, params limited to what the route reads and backed by real fields, create still absent from apiMethods, and that the two empty states naming Create Key carry the action's label.
  • packages/platform-objects/src/identity/two-factor-one-shot-reveal.test.ts: the class guard's ONE_SHOT_ROUTES table gains /api/v1/keys → ['key']. Every action on that route is now held to a covering resultDialog and no successMessage, the same as the 2FA routes. This closes the class rather than adding a single-point pin.
  • packages/services/service-messaging/src/objects/setup-list-empty-states.test.ts (new): every view of the three notification objects declares the copy, the all view declares no projection, filter, sort or page size, and all four bundles carry the label and copy.

Run on 8944600e5 (the PR head, after merging origin/main):

  • pnpm --filter @objectstack/platform-objects test: 72 files / 1128 tests passed.
  • pnpm --filter @objectstack/service-messaging test: 52 files / 584 tests passed.
  • pnpm --filter @objectstack/platform-objects --filter @objectstack/service-messaging typecheck: exit 0. tsc --listFiles confirms all three test files are in a type-check program.

Ablation, one per new pin, each via scripts/ablation-replace.mjs (the anchor hit once, the blob changed, and the restore was proven by blob == HEAD and an empty git diff HEAD). The subjects are imported from src, so no build or dist leg applies:

  1. Deleting Pending's title made 3 tests red: every-view-declares, the Pending pin and the en-bundle equality.
  2. Deleting the { path: 'key' … } reveal field made 2 tests red: the one-shot class guard ("does not reveal 'key'") and the masked-reveal pin.
  3. Changing sys_notification_template's columns: [] to ['topic', 'channel'] made the no-projection pin red.

Gates

The derived list is node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 8944600e5. The run is reconciled with --ran (exit codes recorded): 65 derived, 64 run with exit 0, 1 NOT MEASURED, 0 unrun.

  • NOT MEASURED: pnpm check:dual-build-cjs-loads exited 3 with PREREQUISITE NOT MET, because it reads every package's dist/ and this worktree built only the i18n closure. CI's Lint & Repo Gates runs it.
  • pnpm check:i18n and pnpm check:i18n-coverage exit 0. The coverage log reads "14 config(s), 621 baselined untranslated string(s), none new".
  • The artifact-roster families whose roster sits under one of these paths ran locally with exit 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing and pnpm check:filter-alias-parity.
  • Families CI runs (the fast set, type-check lanes, test-repo) are left to this PR's check-runs.

Acceptance notes (observed, not filed)

  • Subscriptions copy departs from the original card on purpose. The card proposed "Subscribe a role, team or user to a topic". The object's own header records that nothing reads subscription rows at runtime (no recipient expansion is wired), so the empty state says what a row records, "follows a notification topic", and promises no delivery.
  • sys_oauth_application's active view filters disabled == false. The object's comments say better-auth writes disabled only when it is set. If such rows hold NULL, the equals false filter would leave never-toggled applications out of Active. This is not measured here and is an inference from the comment alone. The Active empty state is worded so it stays true either way ("stays active until it is disabled").
  • sys_api_key's header docblock says the raw token is returned "via the auth plugin API". The mint path is the runtime keys domain. This is comment drift and was left untouched.
  • service-messaging's ja-JP / es-ES bundles are otherwise English for these three objects (pre-existing echoes). The new empty-state leaves are translated, so those pages mix languages until the rest is translated.
  • examples and packages/qa/dogfood were not run locally. The sys_api_key dogfood pins (api-key-revoke-lifecycle, api-key-owner-revoke) assert the revoke and restore actions by name, and those actions are unchanged.

Generated by Claude Code

…wn empty states; API Keys gains Create Key

WIP: object metadata only; bundles, tests and changeset follow.

Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi
Co-authored-by: Claude <noreply@anthropic.com>
…ocale bundle; metadata tests; changeset

Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/service-messaging, touching 16 documentable anchor(s).

23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json abb6df0e0109f32c8a51f94a4deaaa4518a0b5a2.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json abb6df0e0109f32c8a51f94a4deaaa4518a0b5a2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6ba79fd0acb60e24871c64d9e7346a947feb2485 — the merge of head 8944600e58305440f80d0448bb629ba558fc3847 into base abb6df0e0109f32c8a51f94a4deaaa4518a0b5a2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6ba79fd0acb60e24871c64d9e7346a947feb2485 && git checkout 6ba79fd0acb60e24871c64d9e7346a947feb2485
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin abb6df0e0109f32c8a51f94a4deaaa4518a0b5a2 8944600e58305440f80d0448bb629ba558fc3847 && git checkout -B drift-repro abb6df0e0109f32c8a51f94a4deaaa4518a0b5a2 && git merge --no-ff 8944600e58305440f80d0448bb629ba558fc3847

node scripts/docs-audit/affected-docs.mjs --json abb6df0e0109f32c8a51f94a4deaaa4518a0b5a2

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs abb6df0e0109f32c8a51f94a4deaaa4518a0b5a2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ✅ ACCEPT: PR #23924 at 8944600e58 · domain:engine seat 2 · session_01TQLy3Yq6vF4SDpTXdr3Uwi · 2026-10-16T13:27Z

Basis: claim 6214846930 on #23852; the cross-lane declaration 6214854461 and its amendment 6216098114 on the domain:services seat post #21118 (no objection on either); the dev's report 6216061848; the seat's own reading of every non-generated hunk and of the tests' assertions, against origin/main.

Checklist

  • Shape. Draft, base main, first line Fixes #23852, Clause-②: no on its own line in the body and the changeset. No other number carries a closing verb.
  • Clause-②: no is truthful. Metadata on six platform objects plus one action; no packages/spec/src/**, no field, index, permission or route change, no governed path. CI plus this in-seat read.
  • Scope. 18 paths, all inside the claim surface or the amendment: the three identity objects' list views and one action, the three notification objects (one all view each, declared only for its emptyState, columns: [], no filter, sort or page size; amendment 6216098114 names the tab-label change), both packages' translation bundles, two new tests and one extended test, the changeset.
  • Item 2's own condition ("the claimant confirms the route, and if a new door is needed, that half is its own question"): met with no new door. The seat read packages/runtime/src/domains/keys.ts on origin/main: POST /keys pins user_id (and the active organization) to the caller and whitelists the body to name plus an optional expires_at. So the button can mint only for the person who presses it, on Setup's list as on the Account app's, and the changeset says exactly that ("the key is created for you").
  • One-shot reveal. two-factor-one-shot-reveal.test.ts's ONE_SHOT_ROUTES gains /api/v1/keys: ['key'], so every action on the mint route must reveal key once; ablation A2 (the reveal field removed) turns 2 tests red.
  • Translations. en regenerated; the zh-CN / ja-JP / es-ES leaves hand-written (AGENTS.md:712); the source-hash files have no net diff. The dev reports check:i18n and check:i18n-coverage exit 0.
  • Evidence. platform-objects 1128 of 1128, service-messaging 584 of 584, typecheck exit 0, at the head after the merge of origin/main. Three ablations each went red and each restore was proven.
  • Changeset, sentence by sentence: "What an admin saw" matches the console fallbacks the card quotes; "What changes" matches the diff, including "the data API still does not create keys" (the create affordance stays off); "Unchanged" matches (no field, index or permission hunk; the two row actions untouched). patch on both packages.
  • Gates on the head. ci-failure.mjs --pr 23924: GREEN, 40 of 40. The six skips (Console Pin Gate, Build Docs, Temporal Conformance (live PG + MySQL), Dogfood Verify CLI, the Dogfood Regression Gate matrix template, Packed-tarball smoke (opt-in)) are each on check-expected-skips.mjs's roster, and each gate is unmet on this diff (no docs path, no needs:pack-smoke label, the full battery runs in the queue build).
  • Landing checks. check-governed-merges --pr 23924: NOT governed, 904 changed lines. mergeable_state clean; git merge-tree against current origin/main (11fea6b00) is clean.

The dev's deviations, accepted: one all view per notification object (the claim's "the list views of" was false: there were none); the label "Create Key" (this object's title-case convention); the Subscriptions copy promises no delivery (the object records that subscriptions are not read at runtime).

Out of scope, one line each (no card: none is a measured defect):

  • sys_oauth_application's active view filters disabled == false; rows never toggled may hold NULL → Acceptance notes (read-only inference, not reproduced).
  • sys_api_key's header docblock says the raw token comes "via the auth plugin API"; the mint path is the runtime keys domain → comment drift, noted.
  • service-messaging's ja-JP / es-ES bundles echo English for the notification objects' labels (pre-existing) → noted.
  • The docs name the Connect an Agent page and POST /api/v1/keys as the mint paths (deployment/environment-variables.mdx:261, ai/connect-mcp.mdx); none says they are the only ones, so the new button falsifies no page → noted.

Next: ready, then auto-merge through the queue.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants