diff --git a/docs/log.md b/docs/log.md index a40a1a58..cba4e4a9 100644 --- a/docs/log.md +++ b/docs/log.md @@ -1,5 +1,10 @@ # Log +## 2026-09-01 (6) + +* **Update**: `web/demo-presentation-mode.md` - two more operator-only surfaces follow the Global-Admin view. The "Internal (Autopilot Monitor)" annotation lane was gated on the identity alone: `visibleLanes()` now takes the view flag as an explicit second argument and combines it with the platform identity, so the rule also covers the read-only Global Reader; the `/annotations` overview additionally filters the rendered rows, because that list is not lane-scoped by the request and an internal row would otherwise surface there anyway. The Decision Inspector button on the session detail page moved to `useGlobalAdminUi()` in the same pass. The IME log pattern controls needed no change - `canEdit` already read `isGlobalAdmin && globalAdminMode`, so demo mode reaches them through the effective flag. Also fixed the retention-override banner: the bare warning glyph inherited the base 16px size against 12px text and sat below the first line, replaced by a `w-4 h-4` SVG whose box matches the `text-xs` line box exactly. + + ## 2026-09-01 (5) * **New**: `web/demo-presentation-mode.md` - the portal can now be shown live without leaking operator internals. The Global-Admin view toggle was only ever read by the nav and the scope hooks; every GA extra inside a tenant page was gated on the identity and stayed on screen when it was switched off - including the `/health-check` Backend/Portal Build cards with commit hashes hyperlinked into the private repo. New `useGlobalAdminUi()` (real GA **and** global view on) replaces the bare identity check at the four visible GA surfaces, so switching the toggle off yields a genuinely tenant-admin-looking view. On top of that a hidden demo mode, armed by a self-consuming `?demo=1` (persisted to localStorage, then stripped from the address bar via `history.replaceState`; `?demo=0` clears it), forces the global view off for every consumer without touching the stored value, hides the toggle itself and the platform role badges, and silently replaces `/admin/*` with `/dashboard` instead of rendering the Access Denied card - whose text would itself reveal the platform area. Admin Mode is deliberately untouched: a tenant admin has it too. `user.isGlobalAdmin` is never faked globally, because that would drag `canEditConfig` along and leave a GA without an own tenant-admin role unable to demonstrate anything. Both levels are presentation only - `TenantConfigValidation` and the `GlobalAdminOnly` 403s are unchanged. diff --git a/docs/web/demo-presentation-mode.md b/docs/web/demo-presentation-mode.md index ec1ed207..88aa8ce6 100644 --- a/docs/web/demo-presentation-mode.md +++ b/docs/web/demo-presentation-mode.md @@ -44,12 +44,17 @@ The stored `globalAdminMode` value is never overwritten — demo mode masks it a | Purple platform section in the sidebar | hidden | hidden | | Cross-tenant data scope, platform notifications | collapsed to the tenant | collapsed to the tenant | | Telegram provider, cert-device-binding toggle, retention escape hatch | hidden | hidden | +| Decision Inspector button on the session detail page | hidden | hidden | +| "Internal (Autopilot Monitor)" annotation lane (session card + `/annotations` list and filter) | hidden | hidden | +| IME log pattern enable/disable switch, "Edit (Global)" button, edit form | already followed the view before this change | read-only, as for a tenant admin | | `/health-check` build + commit cards | hidden | hidden | | "Global Admin" / "Global Reader" badge in the user menu | shows "Admin" instead | shows "Admin" instead | | The Global-Admin toggle itself | still there | hidden | | `/admin/*` reached by a typed URL or bookmark | Access Denied card | silent replace to `/dashboard` | | Admin Mode (delete sessions) | unchanged | unchanged — a tenant admin has it too, so it belongs in an honest demo | +The annotation lane is the one surface where the rule also covers the read-only **Global Reader**: `visibleLanes()` takes the view flag as an explicit second argument and combines it with the platform identity, so a Reader with the view switched off sees the same two lanes a tenant admin sees. The `/annotations` overview additionally filters the rendered ROWS to the visible lanes — the list is not lane-scoped by the request, so an internal row would otherwise still surface there. + An already-configured Telegram channel stays visible and readable at both levels (`components/notifications/ChannelEditor.tsx`): it is the tenant's own channel, a tenant admin sees it too, and hiding its provider would make the next save destroy it. # Not a security boundary @@ -62,4 +67,5 @@ Both levels are presentation. Every rule they mirror is enforced server-side and - `src/Web/autopilot-monitor-web/hooks/useAdminMode.ts` — the three modes, URL consumption, effective `globalAdminMode` - `src/Web/autopilot-monitor-web/hooks/useGlobalAdminUi.ts` — the visible-GA-extras hook - `src/Web/autopilot-monitor-web/components/ProtectedRoute.tsx` — platform-route bounce while presenting +- `src/Web/autopilot-monitor-web/app/sessions/components/sessionAnnotationLogic.ts` — `visibleLanes(user, globalAdminView)`, covered by its `__tests__` sibling - [portal-navigation-prefetch.md](portal-navigation-prefetch.md) — the other portal-wide behaviour switch diff --git a/src/Web/autopilot-monitor-web/app/annotations/page.tsx b/src/Web/autopilot-monitor-web/app/annotations/page.tsx index 24293a6e..a86a01e5 100644 --- a/src/Web/autopilot-monitor-web/app/annotations/page.tsx +++ b/src/Web/autopilot-monitor-web/app/annotations/page.tsx @@ -8,7 +8,7 @@ import { API_BASE_URL } from "@/utils/config"; import { authenticatedFetch } from "@/lib/authenticatedFetch"; import { ProtectedRoute } from "@/components/ProtectedRoute"; import { useAuth } from "@/contexts/AuthContext"; -import { useAggregatedAdminScope } from "@/hooks"; +import { useAdminMode, useAggregatedAdminScope } from "@/hooks"; import { TenantScopeSelector } from "@/components/TenantScopeSelector"; import { GlobalAdminBanner, globalAdminSubtitle } from "@/components/GlobalAdminBanner"; import { @@ -65,7 +65,12 @@ export default function AnnotationsPage() { const scope = useAggregatedAdminScope({ defaultAggregated: true }); const { isGlobalAdmin: crossTenant, routeGlobal, selectedTenantId, effectiveTenantId, scopeInitialized, tenants } = scope; - const lanes = visibleLanes(user); + // Effective Global-Admin view (demo mode forces it off): the platform-internal lane follows the + // view both in the filter dropdown and in the list, so presenting this page shows exactly the + // rows a tenant admin would see. The backend filters the lane for tenant callers regardless. + const { globalAdminMode } = useAdminMode(); + const lanes = visibleLanes(user, globalAdminMode); + const visibleRows = rows.filter((r) => lanes.includes(r.lane?.toLowerCase() as AnnotationLane)); const tenantLabel = (id: string | null | undefined) => tenants.find((t) => t.tenantId === id)?.domainName ?? id ?? "—"; @@ -166,7 +171,7 @@ export default function AnnotationsPage() { {loadError ? (
{loadError}
- ) : rows.length === 0 && !loading ? ( + ) : visibleRows.length === 0 && !loading ? (No annotations yet. Open a session and record a verdict in its{" "} Annotations section — judged sessions @@ -187,7 +192,7 @@ export default function AnnotationsPage() {
- {rows.map((row) => ( + {visibleRows.map((row) => (Overridden by administrator —{" "} {dataRetentionDays === 0