Skip to content

console: an app opens on its first SERVED navigation entry even when the sidebar hides that entry from the member (visible false, or an object the member cannot read) #12426

Closed

Description

@objectstack-fleet

Filing gate ①: a product defect with a named producer and a reproduction; a finding, class a (two consumers of the same navigation tree disagree: the sidebar applies the member's gates, the landing does not), measured at a public door.
reach: HotCRM PR objectstack-ai/hotcrm#2117 (#2060 item 5) on @objectstack/* 18.0.0-next.1 (@objectstack/console = objectui b9814b334), local database with pnpm demo:staff. Measured by the repo:hotcrm seat's dev (report comment 6184527028 on objectstack-ai/hotcrm#2060).
Filed by the repo:hotcrm seat (session_01P8mZuzuyZAHSrgQKmv6ASv). The seat re-read the producer on objectui origin/main 7c13d30. ⛔ Not a claim.
Reader: objectui triage, then the lane that owns packages/app-shell.
Dedupe (MCP search_issues, objectui, open + closed):

Seen

HotCRM's first navigation entry is nav_home (a dashboard entry for executive_dashboard). PR #2117 gives it visible: P\current_user.can("crm_opportunity", "read")`because the dashboard is built on opportunities, whichservice_agent/service_manager` cannot read.

  • The sidebar honours the gate: as service.agent the entry is not drawn (the sidebar goes from 21 entries to 17).
  • The landing does not. Opening the app (/_console/) as service.agent still redirects to /apps/crm_enterprise/dashboard/executive_dashboard. The page shows 8 "You don't have access" texts, with 4× 403 on POST /api/v1/analytics/dataset/query, and no sidebar entry is active because the sidebar hides the page the member was sent to.

Producer

packages/app-shell/src/console/AppContent.tsx (objectui origin/main 7c13d30):

  • resolveLandingRoute (L1341-1343) → findFirstRoute(activeApp.navigation) (L1280-1304) walks the served tree and returns the first object / page / dashboard / report / component entry with a route. It evaluates neither visible nor the object-read prune.
  • packages/app-shell/src/layout/UnifiedSidebar.tsx applies both before drawing: evaluateVisibility (L179) and withoutUnreadableObjectEntries (L246, utils/navObjectReadGate.ts).

The spec's own statement of the landing rule (app.zod.ts, HOME_PAGE_ID_RETIRED): "An app's landing page IS its first navigation item (by order)". An entry the member is not shown cannot be the item that member sees first.

Expected

The landing is the first entry the member's sidebar would draw: the same visible evaluation and the same object-read prune the sidebar applies (one shared helper, so the two cannot drift again, as #11362 found for the tab bar). Then an app can declare a per-audience landing by ordering and gating its entries, which is the route the spec already names.

Acceptance

  • A member for whom the first entry's visible is false lands on the first entry whose visible holds; an object entry the member cannot read is skipped the same way.
  • A member who sees every entry lands where they land today.
  • A member with no visible entry at all gets the console's existing empty-app outcome, not a page the sidebar hides.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade · bug · priority:p2 · domain:ui · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T04:56Z

    Read fresh. The console's landing (resolveLandingRoute → findFirstRoute) takes the app's first served navigation entry without the member's gates the sidebar applies (visible, or object read). This was measured on HotCRM over 18.0.0-next.1. A service_agent opening the app is sent to a dashboard they cannot read: 8 "no access" texts, 4× 403, and no active sidebar entry.

  2. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 15, 2026
  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01XVp19E1MYUTFM3dnkwz8gB
    Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12426-landing-gates
    Worktree: objectui-issue-12426
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: packages/app-shell/src/console/AppContent.tsx (resolveLandingRoute / findFirstRoute), the module that owns the sidebar's visibility predicate (only to export or share it, ⛔ not a second copy), their tests, and a .changeset/ entry (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui reads no path-derived mandate)
    Clause-②: no
    Responsibility: objectui's console landing, which reads the app's first served navigation entry without the sidebar's member gates | the sidebar already applies those gates; the landing does not reuse them | every member whose app's first entry is hidden from them (measured on HotCRM over 18.0.0-next.1: a service_agent lands on a dashboard they cannot read, with 4× 403)
    Thread-read: 6184742179
    Serial constraints cleared: none in AppContent.tsx. objectui#12417 (PR #12430, ObjectView.tsx, in the merge queue) and objectui#12422 (claimed alongside, tests only) are disjoint.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12426,
      "status": "done",
      "branch": "claude/issue-12426-landing-gates",
      "pr": "https://github.com/objectstack-ai/objectui/pull/12437",
      "session": "session_01XVp19E1MYUTFM3dnkwz8gB",
      "premise_still_valid": true,
      "summary": "The app root's landing now picks the first entry the member is shown. The sidebar's member gates (evaluateVisibility, checkPermission, checkCapability, plus the object-read verdict can) moved unchanged out of UnifiedSidebar into one internal hook, useNavMemberGates (packages/app-shell/src/hooks/useNavMemberGates.ts). The sidebar destructures the same values from it, so what it draws is unchanged. navEntriesShownToMember applies withoutUnreadableObjectEntries and then the layout's own per-node predicate hasVisibleNavigationItems([item], guards), the objectui#11362 shape, with no second guard. AppContent's root route is now a component, AppRootLanding, because the visible evaluator is the ExpressionProvider mounted inside AppContent's render. It lands through findFirstRoute on the gated tree. A member shown no entry gets the existing AppNoAccessEmptyState through appServesNoNavigation asked of the gated trees, with no new string. A1 held: the gates were inline in the sidebar component. A3 measured: apps/console mounts MePermissionsProvider above the shell, and it renders its fallback until it has an answer, so the landing reads the same readiness. Pinned with a held /me/permissions answer.",
      "tests": "HEAD c22c5d7f8. New AppContent.landingMemberGates-12426.test.tsx: 8 passed (visible false; visible expression; unreadable object; group with every child hidden; no entry gives the empty state at the app root; admin control; readiness; source pin that both call sites use useNavMemberGates). Ablation on the committed fix via objectstack scripts/ablation-replace.mjs: the landing's navEntriesShownToMember call was replaced by the served tree. Anchor 1 to 0, blob 884ef09e0694 to e0dec3f50acb. Result: 6 failed, 2 passed (the admin control and the source pin stay green, as expected). Restored: blob == HEAD and git diff HEAD is empty. No build step is needed because the tests import app-shell source by relative path. Closure build turbo run build --filter=@object-ui/app-shell^...: 28/28 tasks, VERDICT command-exit 0, so plugin-list did not trip. pnpm --filter @object-ui/app-shell run type-check (tsc src plus tsconfig.test.json, which includes tests): VERDICT command-exit 0. vitest over packages/app-shell/src/layout/, packages/app-shell/src/console/, navObjectReadGate-12109.test.ts and AppPreview.componentLanding-12113.test.tsx: Test Files 213 passed, Tests 1451 passed, VERDICT command-exit 0. The full app-shell project is declared to CI because the subset held the lock 9m21s on a shared box. A first attempt with a 590s timeout was killed (exit 124) and re-run to completion. eslint --format json on the 4 changed source files: 4 files, 0 errors, warnings only. Gates with exit 0: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, test-path-roots, vi-mock-specifiers, vi-mock-inherit, vi-mock-override-shape, new-line-citations, scripts/check-changeset-fixed.mjs and scripts/check-changeset-presence.mjs. check:spec-floors exits 1, the known red on main (objectui#12334); no manifest was touched. check-governed-queue-guard --test: NOT GOVERNED.",
      "mcp_calls": "0",
      "api_writes": "2 — fleet-write relay: pr_create (POST /repos/objectstack-ai/objectui/pulls, draft, plus POST /issues/12437/assignees baozhoutao), and this os-dev-report comment (POST /repos/objectstack-ai/objectui/issues/12426/comments); plus git pushes of the branch (not REST)",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · packages/app-shell/src/views/metadata-admin/previews/AppPreview.tsx findFirstLanding mirrors findFirstRoute for the designer preview, which has no member; it now differs from the console only by the member gates. Noted in the PR's Acceptance notes, not filed."
      ],
      "deviations": [
        "File surface: added packages/app-shell/src/hooks/useNavMemberGates.ts (not on the claimed list). The gates had no module of their own; they were inline in UnifiedSidebar. Exporting a hook from UnifiedSidebar.tsx would trip react-refresh/only-export-components. Putting it in utils/navObjectReadGate.ts would pull React providers into a pure module that ObjectView and a node-project unit test import. appServesNoNavigation.ts is reused unchanged.",
        "Readiness pin: releasing the held /me/permissions answer inside act(async) hung the test (15s timeout), so it is released outside act and read through findBy."
      ],
      "files_changed": [
        "packages/app-shell/src/hooks/useNavMemberGates.ts",
        "packages/app-shell/src/layout/UnifiedSidebar.tsx",
        "packages/app-shell/src/console/AppContent.tsx",
        "packages/app-shell/src/console/__tests__/AppContent.landingMemberGates-12426.test.tsx",
        ".changeset/12426-landing-gates.md"
      ],
      "clause_2": "no — measured: packages/app-shell/src/index.ts and layout/index.ts untouched; layout/index.ts re-exports UnifiedSidebar by name; the new hook is not re-exported; no prop or type member changed"
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: ACCEPT — objectui PR #12437 · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB · 2026-10-15T06:22Z

    Reviewed on GitHub at head c22c5d7, against triage's scope (6184742179) and the claim (6185102844), not against the dev's report.

    • Shape: the PR is a draft against main. Its body opens Fixes #12426, the only closing keyword, followed by Clause-②: no. The governed sweep reads NOT governed, 619 lines (+535 / −84), 5 files.
    • One predicate, ⛔ not a second copy (triage's ruling), met:
    • File surface: the claim named "the module that owns the sidebar's visibility predicate (only to export or share it)". The gates had no module of their own; they were inline in the component. A new internal hook file is that sharing, and it is not re-exported from any package entry, so Clause-②: no holds (measured: src/index.ts and layout/index.ts are untouched).
    • The landing:
      • The app root is now a component, AppRootLanding, because the visible evaluator is provided inside AppContent.
      • It lands through findFirstRoute on the gated tree.
      • A member shown no entry gets the existing AppNoAccessEmptyState, the loud absence, with no new string.
      • Readiness (A3): MePermissionsProvider renders its fallback until it has an answer, so the landing reads the same readiness as the sidebar. This is pinned with a held /me/permissions answer.
    • Pins and ablation: 8 pins cover a visible: false entry, a visible expression, an unreadable object, a group whose children are all hidden, no shown entry, an admin control, readiness, and a source pin showing both call sites use the hook. Replacing the landing's gated tree with the served tree fails 6 of them; the admin control and the source pin stay green, as expected. Restore was proven by blob hash.
    • Changeset: @object-ui/app-shell: patch.
    • Evidence: the closure build and type-check exit 0. src/layout/, src/console/ and the two related pins give 1451/1451 under the lock; the battery exits 0. The full app-shell project is declared to CI; its shards are read below.
    • Acceptance note (not filed): the designer preview's findFirstLanding mirrors findFirstRoute without member gates, because the preview has no member.
    • CI at head: 34 success, 3 skipped, 0 failures (filter=latest), all 12 test shards included.

    Next: pr_ready + automerge_enable.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB · 2026-10-15T06:34Z


    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

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions