The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.25.0) is the single operator UI for the human CEO: it drives intake/prompter chats, task/kanban management, agent observability, metrics, release approval, playbook curation, and feature-flag arming. It is served internally on port 3000 behind the nginx reverse proxy and talks to the orchestrator exclusively over relative `/api` + `/ws` URLs.
| `panel/src/app/(dashboard)/tasks/page.tsx` + `tasks/[taskId]/page.tsx` | Task list, now with top-level **List\|Kanban tabs** (#694/#688) sharing the page's URL filter state — List renders the existing `TaskFilters`+`TaskTable` unchanged, Kanban embeds the four kanban views (dev/qa/pr-review/pm) as nested sub-tabs, a controlled team filter shared bidirectionally with the List tab; + task detail (tabbed) |
| `panel/src/app/(dashboard)/kanban/page.tsx` | **Redirects to the Tasks page's Kanban tab** (#694:`feat(panel): redirect /kanban to Tasks kanban tab, remove sidebar entry, swap bottom tab bar to Agents`) — the standalone kanban surface is gone from the sidebar; `KanbanBoard`'s dnd-kit drag-and-drop and mobile single-column nav are unchanged, just reached from `tasks?tab=kanban` now |
| `panel/src/app/(dashboard)/projects/[projectId]/settings/page.tsx` | **Project settings full page** (Wave C, replacing the ~30-field `edit-project-dialog.tsx`, which is deleted): a card-per-concern grid — `identity-card` (name/slug/Forge/GitHub App), `git-auth-card` (PAT), `placement-card` (cell/team, protected branches), `environments-card` (env ladder), `cicd-commands-card`, `sandbox-card` (services + extensions), `budget-ops-card` (monthly budget, video engine toggle, CI-watch/dep-update/ci-watch toggles) — each with its own per-card save (`save-bar.tsx`), plus Conventions as a page-level tab at natural width instead of a cramped dialog tab |
| `panel/src/app/(dashboard)/agents/page.tsx` | Agents hub: Fleet (roster/spawn/status, default) + Conversations + Journals tabs via `?tab=fleet\|conversations\|journals` — `AgentsFleetView` (fleet roster/spawn/activity), `A2AView` (org-wide switchboard/list + transcript + CEO reply composer + "New DM" dialog, live via `/ws/system``a2a.message` frames — a pure lift of the old standalone `/a2a` page body), and `JournalsView` (per-agent reflection log — a pure lift of the old standalone `/journals` page body, wave 11) |
| `panel/src/app/(dashboard)/a2a/page.tsx` | Redirect shim → `/agents?tab=conversations` (A2A folded into the Agents hub's Conversations tab, wave 9) — kept so old bookmarks/links still resolve |
| `panel/src/app/(dashboard)/journals/page.tsx` | Redirect shim → `/agents?tab=journals` (Journals folded into the Agents hub's Journals tab, wave 11) — kept so old bookmarks/links still resolve; `journals/[entryId]/page.tsx` (entry detail) is unchanged at its own route, only its back-links now point at `/agents?tab=journals` |
| `panel/src/app/(dashboard)/workstation/page.tsx` | Workstation: Products + Projects merged as URL-param tabs (`?tab=products\|projects`, Products first); `products/page.tsx` and `projects/page.tsx` are now server-component redirects to it |
| `panel/src/app/(auth)/login/page.tsx` | Cloud-auth login form (email/password → `useLogin` → `/auth/login`); only reachable/relevant once `proxy.ts` starts gating the `(dashboard)` group |
| `panel/src/app/(tg)/layout.tsx` + `(tg)/tg/page.tsx` | Telegram Mini App cockpit at `/tg`: slim shell (`id="tg-shell"` theme-scoping hook, `--tg-viewport-stable-height` sizing, `next/font/local` Share Tech Mono loaded as `--font-share-tech`, `next/script` loads the Telegram WebApp bridge `afterInteractive`) + bootstrap page that resolves `window.Telegram.WebApp`, POSTs its `initData` to `/telegram/webapp-auth` unconditionally, then renders the tabbed cockpit (or an "Open from Telegram" wall for ANY non-dev-mock bridge with empty `initData` — closes a prod-only bug where a plain-browser visit still gets the WebApp bridge object but empty `initData`, 422ing into "Couldn't sign in") |
| `panel/src/proxy.ts` | Next 16's rename of `middleware.ts`: probes `/auth/status` (docker-internal orchestrator URL, fails open to "off" on any error/timeout) and redirects to `/login` when cloud auth is on and no session cookie is present; matcher excludes `tg(?:/|$)` — the Mini App authenticates via Telegram `initData`, not the password cookie, so it must never be redirected to `/login` |
| `panel/src/components/dashboard/` | Overview cards: command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity, `x-post-queue.tsx`, `video-post-queue.tsx`, `roadmap-review-queue.tsx`, `project-badge.tsx` (shared project/repo badge — renders nothing when neither slug nor name is set; used by both the X and video post-queue rows so a multi-project CEO can tell drafts apart) |
| `panel/src/components/metrics/` | delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table, `scorecards-tab.tsx` (calls `useAllMemberScorecards()` once instead of per-row `useMemberScorecard` — see `docs/map/metrics-observability.md`) |
| `panel/src/components/kanban/{core,shared,views}/` | core: `kanban-board.tsx` (`tasksByStatus` grouping memoized, `handleAction` stabilized via `useCallback` + a `tasksRef`), `kanban-card.tsx` (wrapped in `memo`), `kanban-column.tsx` (windowed via `@tanstack/react-virtual`'s `useVirtualizer`, also `memo`-wrapped — the column itself stays the dnd-kit droppable target so windowing doesn't break drag targeting) + bypass-preconditions; views: dev/qa/pm/pr-review kanban |
| `panel/src/components/a2a/` | a2a-view.tsx (`A2AView` — the Conversations tab's full body, a pure lift of the old standalone `/a2a` page; owns the `?dm=` quick-action latch, see Gotchas), a2a-switchboard (org-chart pair cards, 45s pulse fade) + a2a-switchboard-utils (pairKey/grouping/pulse), a2a-pair-card, a2a-conversation-list (classic fallback), a2a-transcript, a2a-reply-composer (CEO chime-in on a watched conversation), a2a-new-dm-dialog (CEO opens a fresh 1:1, or preselects a validated `?dm=` deep-link target; exports `EXCLUDE_NON_DM_ROLES` so `tg-chat-tab.tsx`'s compose picker shares the same non-DM-capable-role exclusion instead of drifting out of sync), a2a-direct-composer (CEO's own thread, no task link required), a2a-utils |
| `panel/src/components/tasks/` + `tasks/task-detail/` | task-table (row/card extraction into `memo`-wrapped `TaskTableRow`/`TaskTableCard`, `toggleExpand` stabilized via `useCallback` — pagination-based, not virtualized), create/edit-task-dialog, task-filters, acceptance-criteria-editor, dependency-selector, task-detail tabs (overview/plan/progress/commits/sessions/notes/dependencies/**findings**), `mobile-task-board.tsx` (read-only, grouped-by-status phone board for the `/tg` cockpit; gained `tasks`/`onTaskPress` props for demo-mode + the task detail sheet) |
| `panel/src/components/tg/` | The `/tg` cockpit's tabs — now **5-tab** bottom nav (`today` default + `approvals`/`inbox`/`board`/`chat`): `tg-today-tab.tsx` (the default-opening "Today" brief — spend hero with 7-day sparkline, quick-action ring, needs-you banner, live fleet avatars, velocity bars), `tg-approvals-tab.tsx` (native card stack over `approvals/` — 7-file subdirectory: `use-approval-queue.ts`, `primary-action.tsx`, `reject-form.tsx`, `release-detail.tsx`, `x-post-detail.tsx`, `video-post-detail.tsx`, `roadmap-item-detail.tsx`), `tg-inbox-tab.tsx` (notifications + ack, `TgAvatar` sender tokens), `tg-board-tab.tsx` (wraps `mobile-task-board.tsx` + the new `tg-task-sheet.tsx` detail sheet), `tg-chat-tab.tsx` (WS-live via `/ws/system`, 10s poll fallback only while the socket is down), `ui.tsx` (shared visual-language primitives: `TgCircleAction`/`TgAvatar`/`TgSection`/`TgRow`/`TgRowIcon`/`TgStat`), `charts.tsx` (hand-rolled inline-SVG `Sparkline`/`DayBars`, no charting lib in the Mini App bundle), `tg-icons.tsx` (9-icon hand-drawn duotone set for hero surfaces, utility chrome stays lucide-react), `motion.tsx` (`useCountUp` numeral count-up hook + `TgSheet` bottom-sheet dialog wired to Telegram's native `BackButton`), `tg-task-sheet.tsx` (read-only task-detail bottom sheet: status/bounced-chip/ACs/up-to-5-open-findings/PR link) |
| `panel/src/lib/telegram/webapp.ts` | Typed wrapper over the global `window.Telegram.WebApp` (`ready`/`expand`/`initData`, `TelegramThemeParams`/`MainButton`/`BackButton`/`HapticFeedback` interfaces); `waitForTelegramWebApp` polls (100ms, 1.5s timeout) for the CDN script since it loads `afterInteractive`; `createDevMockWebApp`/`isDevMockWebApp` back the `/tg?demo=1` dev-browser fallback; null-safe `haptics` const (no-ops outside Telegram) |
| `panel/src/lib/telegram/{hooks.tsx,theme.ts,demo.ts,demo-data.ts}` | `hooks.tsx`: `TgWebAppProvider`/`useTgWebApp` context + `useMainButton`/`useBackButton` (declarative wrappers over Telegram's native buttons — no consumer touches `window.Telegram` directly). `theme.ts`: `applyTelegramTheme` maps `themeParams` → shadcn CSS vars, hex-only trust boundary, scoped to `#tg-shell` only (the desktop dashboard is untouched); `startTelegramThemeSync` re-applies on the bridge's `themeChanged` event. `demo.ts`/`demo-data.ts`: `isTgDemoMode()` (dev-only, `?demo=1`, dead-code-eliminated in prod) gates dynamically-imported `DEMO_TASKS`/`DEMO_NOTIFICATIONS`/`DEMO_RELEASE`/`DEMO_X_POSTS`/`DEMO_VIDEO_POSTS`/`DEMO_ROADMAP`/`DEMO_TODAY` fixtures |
| `panel/src/components/conventions/conventions-tab.tsx` | Per-project architecture map + health — now a page-level tab on `/projects/[id]/settings` (Wave C; was a cramped tab inside the deleted edit-project dialog) |
| `panel/src/components/projects/settings/*.tsx` | Wave C card-per-concern settings surfaces (`identity-card`, `git-auth-card`, `placement-card`, `environments-card`, `cicd-commands-card`, `sandbox-card`, `budget-ops-card`, `save-bar.tsx`) backing `/projects/[id]/settings`, replacing the single ~1061-line `edit-project-dialog.tsx` (deleted) |
| `panel/src/components/projects/quick-edit-project-dialog.tsx` | Wave C: slim replacement for the old kitchen-sink edit dialog — name/cell/active only; the project list's Edit action now routes to the full settings page instead |
| `panel/src/components/layout/command-palette.tsx` + `hooks/use-command-palette.ts` | Wave C (#694/#670): global Cmd+K palette — Radix Dialog + combobox searching tasks/agents/projects/pages, `localStorage` recents (`lib/command-palette-recents.ts`, key `roboco-cmd-recents`), fuzzy match (`lib/fuzzy-match.ts`), keyboard nav; mounted globally in the dashboard layout, wired to the header search trigger via `useUIStore.setCommandPaletteOpen` (`components/layout/header.tsx`) |
| `panel/src/components/ui/collapsible-section.tsx` | Wave C dialog-standardization: the single sectioned-disclosure primitive, moved from `tasks/task-detail/` to `ui/` — task dialogs' raw `Collapsible` and create-project's ad-hoc `showAdvanced` both converge onto it. Every hand-rolled dialog footer across the panel became `DialogFooter`; dialog widths collapsed from ten ad-hoc classes to three named sizes (deliberate outliers annotated). No behavioral change. |
| `panel/src/components/projects/`, `products/`, `agents/`, `business/`, `auditor/`, `knowledge-base/`, `git/`, `journals/`, `work-sessions/`, `notifications/`, `rate-limit/`, `layout/`, `ui/` | Per-domain component groups (`projects/` and `products/` each export a `*-view.tsx` consumed by `workstation/page.tsx`, plus a `*-card-grid.tsx` reusing the sibling table's exported badge renderers; `agents/` similarly exports `agents-fleet-view.tsx` (`AgentsFleetView`) consumed by `agents/page.tsx`'s Fleet tab, plus `agent-card.tsx`'s DM quick-action button; `journals/` similarly exports `journals-view.tsx` (`JournalsView`) consumed by `agents/page.tsx`'s Journals tab); `ui/` = Radix-based primitives (dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown, `collapsible-section`) |
| Release Proposal | `components/dashboard/release-proposal-card.tsx` | CEO approve/reject-with-changes on held `release_manager` proposal; fail-closed executor; hidden on 404, retry on real error |
| Playbook Review Queue | `components/dashboard/playbook-review-queue.tsx` | Auditor/CEO approve/reject drafted playbooks; hidden when no drafts |
| CEO Approval Queue | `components/dashboard/ceo-approval-queue.tsx` | Tasks in `awaiting_ceo_approval` awaiting CEO verdict |
| PR Review Queue | `components/dashboard/pr-review-queue.tsx` | Inbound external/fork PRs + in-path gate PRs for the reviewer |
| X Post Queue | `components/dashboard/x-post-queue.tsx` | CEO edit/approve (posts to X)/reject on held release-post + mention-reply drafts; hidden when empty |
| Roadmap Review Queue | `components/dashboard/roadmap-review-queue.tsx` | CEO per-item approve (materializes BACKLOG task)/reject on the Product Owner's held roadmap cycle; hidden until authored |
| Agents — Fleet | `app/(dashboard)/agents/page.tsx` (`?tab=fleet`, default) + `components/agents/agents-fleet-view.tsx` + `components/agents/*` | Full agent roster grouped into Leadership/Backend/Frontend/UX-UI/Support grids: live state, spawn/stop controls, activity-stream link, token usage. Each card's DM button (hidden for `EXCLUDE_NON_DM_ROLES`) deep-links `/agents?tab=conversations&dm=<agent id>` into the Conversations tab |
| Agents — Conversations (switchboard + reply + New DM) | `app/(dashboard)/agents/page.tsx` (`?tab=conversations`) + `components/a2a/a2a-view.tsx` + `components/a2a/*` | CEO watches every agent-to-agent conversation live: default org-chart switchboard (pair cards grouped by cell/PM-chain/board, pulsing on fresh `a2a.message` frames) or the classic conversation list; drill-in shows the transcript + a reply composer that lets the CEO chime into a watched thread as itself (task-linked conversations only). "New DM" opens a fresh CEO-owned 1:1 with any DM-capable agent (no task link needed) — either from the dialog's own picker or preselected from a validated `?dm=` deep link off the Fleet tab; the recipient is woken via the a2a_request dispatch path if offline, and the CEO's own threads render with `A2ADirectComposer` instead of the reply composer. `/a2a` redirects here |
| Agents — Journals | `app/(dashboard)/agents/page.tsx` (`?tab=journals`) + `components/journals/journals-view.tsx` + `components/journals/*` | Per-agent reflection log lifted into the Agents hub's third tab: agent picker (left, local search state seeded from the last saved query) + a type/task-filterable entry list (right, `JournalView`); each task-linked entry carries a Task badge + copy-UUID button. Entry detail still lives at `/journals/{entryId}`, its back-links now pointing at `/agents?tab=journals`. `/journals` redirects here |
| Workstation | `app/(dashboard)/workstation/page.tsx` + `components/{products,projects}/*-view.tsx` | Products + Projects as one sidebar entry, tab-switched via `?tab=`; each surface has a Cards\|Table view toggle (default Cards, persisted per-surface via `ui-store`'s `productsView`/`projectsView`) with client-side name/cell(-count) sorting in card view; Projects' q/cell/inactive filters are local `useState`, not URL params (scroll-bounce prevention) |
| Project Settings / Conventions | `app/(dashboard)/projects/[projectId]/settings/page.tsx` + `components/projects/settings/*.tsx` + `components/conventions/conventions-tab.tsx` | Wave C full-page project settings (card-per-concern, per-card save) — was the ~30-field `edit-project-dialog.tsx` (deleted). The list's Edit action routes here; a slim `quick-edit-project-dialog.tsx` (name/cell/active) covers the fast path. Conventions is a page-level tab: per-project `.roboco/conventions.yml` map + health, Save/Restore via PR. The "Forge" `<Select>` (Auto-detect/GitHub/Gitea/GitLab, `project.git_provider`) now lives on `identity-card.tsx`; protected branches on `placement-card.tsx`; env ladder on `environments-card.tsx`; video engine + budget on `budget-ops-card.tsx` — see `docs/map/worksession-git.md` |
| Command Palette | `components/layout/command-palette.tsx` + `hooks/use-command-palette.ts` | Global Cmd+K search over tasks/agents/projects/pages with recents + fuzzy match; triggered from the header search box |
| Git | `app/(dashboard)/git/page.tsx` | Repository / Work Sessions tabs (business-page tab idiom, `?tab=`); `GitBrowser` (status/branches/log/diff + actions incl. confirm-gated "Clean Up Stale Branches") and `WorkSessionsView` (active sessions, search/status filter kept LOCAL not in URL params); old `/work-sessions` route now redirects to `/git?tab=sessions` |
| Kanban | `app/(dashboard)/tasks/page.tsx` (Kanban tab) + `components/kanban/{core,views}/*` | dnd-kit drag board; dev/qa/pm/pr-review views; drag routes through admin status-override with bypass-precondition prompt. Reached via the Tasks page's **List\|Kanban tabs** (#694); the standalone `/kanban` route now redirects here and its sidebar entry is gone (bottom tab bar swapped to Agents) |
| `useA2AAdminPairs` / `useA2AConversations` / `useA2AMessages` | hooks | `hooks/use-a2a-live.ts` | TanStack Query wrappers over `a2aApi.listAdminPairs/listAdminConversations/listAdminMessages`; 30s `staleTime`, invalidated by `a2a.message` frames; `useA2AMessages` takes an optional `{ refetchInterval }` (default off — the desktop Conversations tab relies on WS invalidation) that `tg-chat-tab.tsx` now uses only as a fallback (`THREAD_POLL_MS=10_000`) while `/ws/system` is down, since it also gained its own `useA2ALiveStream()` WS invalidation in the V4 premium-cockpit pass |
| `A2AView` | comp | `components/a2a/a2a-view.tsx` | Conversations tab body — pure lift of the old standalone `/a2a` page (switchboard/list, transcript, reply/direct composer, New DM); owns the `?dm=` quick-action latch (`dmParam`/`prevDmParam` render-phase state, re-arms once `dm` is stripped) and the `?conversation=` selection, both now targeting `/agents` instead of `/a2a` |
| `AgentsFleetView` | comp | `components/agents/agents-fleet-view.tsx` | Fleet tab body — pure lift of the old standalone `/agents` page (roster grids, orchestrator status, spawn/stop controls); `AgentCard`'s DM button pushes the `?dm=` deep link the Conversations tab's latch consumes |
| `JournalsView` | comp | `components/journals/journals-view.tsx` | Journals tab body — pure lift of the old standalone `/journals` page (agent picker + filterable entry list); its `agent`/`type`/`task` URL params keep working, every writer now targets `/agents` (not `/journals`) preserving the rest of the query string (e.g. `tab=journals`), mirroring the `A2AView` idiom; the agent search box stays local `useState`, not URL-synced, so a per-keystroke filter doesn't bounce scroll position |
| DM quick-action (`?dm=` deep link) | logic | `components/agents/agent-card.tsx` + `components/a2a/a2a-view.tsx` + `components/a2a/a2a-new-dm-dialog.tsx` | `AgentCard`'s DM button (hidden for `EXCLUDE_NON_DM_ROLES`) pushes `/agents?tab=conversations&dm=<agent id>`; `A2AViewContent` latches it open once per distinct value then strips `dm` from the URL, re-arming (`prevDmParam` reset to `null`) so a repeated identical deep link still fires instead of being silently swallowed by a stale latch; `A2ANewDmDialog` only preselects the target once it resolves against the live roster and passes `EXCLUDE_NON_DM_ROLES` — the deep link is untrusted URL input, never trusted blindly |
| `A2ASwitchboard` / `A2APairCard` | comp | `components/a2a/a2a-switchboard.tsx` + `a2a-pair-card.tsx` | Org-chart pair cards grouped into sections (cell/PM-chain/board/cross-team) via `groupPairsBySection`; each card pulses for `PAIR_PULSE_FADE_MS` (45s) after a matching live frame |
| `A2AReplyComposer` | comp | `components/a2a/a2a-reply-composer.tsx` | CEO chime-in box on a selected WATCHED conversation; disabled when it has no linked task (A2A sends require one) |
| `A2ANewDmDialog` | comp | `components/a2a/a2a-new-dm-dialog.tsx` | CEO-voiced "start a fresh 1:1" entry point; `AgentSelector` with `excludeRoles` dropping self + non-`read_a2a` roles (auditor/pr_reviewer/prompter/secretary); `useCreateCeoConversation` opens the thread and sends the first message in one call. A controlled `open`/`onOpenChange` pair plus `initialTarget` let the Fleet tab's DM quick-action drive it externally — `initialTarget` only preselects once validated against the live roster + the same exclusion (untrusted `?dm=` input, never trusted blindly) |
| `A2ADirectComposer` | comp | `components/a2a/a2a-direct-composer.tsx` | Composer for a conversation the CEO itself owns (`agent_a`/`agent_b === "ceo"`); posts via `useSendCeoMessage`/the plain per-conversation send route, NOT the interject-as-ceo route `A2AReplyComposer` uses — no task link required |
| `useCreateCeoConversation` / `useSendCeoMessage` | hooks | `hooks/use-a2a-live.ts` | Mutations wrapping `a2aApi.createConversation`/`sendCeoMessage`; both force `X-Agent-ID: "ceo"` (literal slug, not the UUID) on the request and invalidate the conversation list (+ that conversation's messages for the send) |
| `QUICK_ACTIONS_REGISTRY` | const | `components/dashboard/quick-actions-registry.ts` | 23-action catalog (id/label/icon/href/tip) derived from the sidebar route surface + tab-parameterized deep links; `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy `QuickActionsBar` destination; `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids; the `journals` and `a2a` entries' `href`s retarget to `/agents?tab=journals` / `?tab=conversations` (waves 11 and 9) now that their standalone pages are redirect shims |
| `GitActionsPanel` | comp | `components/git/git-actions-panel.tsx` | Commit/push/PR/rebase actions + destructive-confirm "Clean Up Stale Branches" (`AlertDialog`) |
| `useCleanupBranches` / `handleCleanupBranches` | hook | `hooks/use-git.ts` / `hooks/use-git-browser.ts` | Mutation over `POST /git/branches/cleanup`; the browser hook tracks a per-project cursor ref so a repeat click resumes a truncated sweep instead of re-scanning the first window |
| `WorkSessionsView` | comp | `components/work-sessions/work-sessions-view.tsx` | Git page's "Work Sessions" tab body; search/status filters are LOCAL `useState`, not URL params |
| `CostTrendChart` / `SpendTrendChart` | comp | `components/dashboard/cost-trend-chart.tsx` / `components/business/spend-trend-chart.tsx` | Daily-spend area charts off `GET /usage/time-series`; 7d on Overview (`CommandCenter`), 30d on the Business scorecard (`CompanyScorecardCard`) |
| `CompanyScorecardCard` / `ObjectivesSection` | comp | `components/business/company-scorecard-card.tsx` | Business page Scorecard tab body; four sections off one `cockpitApi.summary()` call. `ObjectivesSection` renders three positional charter objective cards (`first_pass_yield` 90%, `median_lead_time_hours` <24h, `escaped_defects` 0) — positional-by-convention mapping documented in a `ponytail:` code comment; "No data yet" fallback for null/undefined metrics (mirrors `SpeedSection`). `CockpitSummary` gained optional `first_pass_yield?: number\|null` and `escaped_defects?: number\|null` (backend companion item not yet shipped — see `panel/docs/frontend/company-scorecard-card.md`). |
| `sortProducts` | fn | `components/products/products-view.tsx` | Pure client-side sort (name/cell count) for the Products card grid; direction rides a comparator multiplier, not sort-then-reverse, so ties keep their relative order |
| `sortProjects` | fn | `components/projects/projects-view.tsx` | Pure client-side sort (name/cell) for the Projects card grid; same multiplier pattern, plus a `teamLabels[cell] ?? String(cell)` fallback so a backend `Team` value outside the panel's map never crashes the sort |
Browser → nginx :3000 → (panel Next.js server for pages; `/api/*` and `/ws/*` proxied to `orchestrator:8000`). All client calls use relative URLs: `API_URL="/api"` (axios `baseURL`) and `WS_URL="/ws"` (`getWebSocketUrl`) — no CORS because the browser sees one origin. When cloud auth is armed (`ROBOCO_CLOUD_AUTH_ENABLED`), every navigation to a `(dashboard)` route first runs `proxy.ts` (Next 16's rename of `middleware.ts`), which probes `/auth/status` directly against the docker-internal orchestrator URL (not through nginx) and redirects to `/login` when no `roboco_session` cookie is present; a probe failure/timeout fails OPEN to "cloud auth off" so a slow/unreachable backend never blocks navigation. The login page (`(auth)/login/page.tsx`) posts credentials via `authApi.login` (OAuth2 form body, FastAPI Users' cookie route) and the session cookie rides back on the response. The shared axios client DEFAULTS `X-Agent-ID=<CEO_AGENT_ID>` + `X-Agent-Role=CEO_ROLE` headers for API authorization — `has()`/`set()`, not a flat overwrite, so a call that already set its own headers (the CEO-DM composer's `X-Agent-ID: "ceo"`, needed literally by its route) keeps them. Live events flow: orchestrator `StreamEventBus` → `websocket_bridge` → per-resource `/ws/{agents,notifications,system}` sockets → panel `useWebSocket` hooks → zustand stores / TanStack Query cache. Usage snapshots (`USAGE_SNAPSHOT`) and rate-limit lifecycle (`RATE_LIMIT_HIT/LIFTED`) arrive on the single shared `/ws/system` stream mounted in providers; on any non-`connected` state the usage store clears its snapshot so the panel falls back to HTTP-polling summary until a fresh frame lands. The Agents hub's Conversations tab (`useA2ALiveStream`) is a second, independent consumer of that same shared `/ws/system` connection (not a new socket): every persisted A2A message publishes an `a2a.message` frame, which the tab uses purely to invalidate-on-frame (REST via `a2aApi` stays the source of truth for full message bodies, since the frame's excerpt is capped) and to drive the switchboard's 45s pulse fade on the matching pair card.
Deliberately **not** on this card (compose/env-coupled, unsafe for a runtime toggle): `ROBOCO_CLOUD_AUTH_ENABLED`, `ROBOCO_DB_NETWORK_ISOLATED`, and `ROBOCO_TELEGRAM_MINIAPP_ENABLED` (Mini App sign-in — same TLS coupling as cloud auth, which it also requires).
- **Relative URLs only** (`/api`, `/ws`); overriding `NEXT_PUBLIC_API_URL`/`NEXT_PUBLIC_WS_URL` to an absolute URL reintroduces CORS — leave defaults.
- **`/ws/system` is a single shared instance** mounted once in providers; a second `useWebSocket("/system")` would open a second socket. `getWebSocketUrl` already supplies `/ws`, so pass only the path (passing `/ws/system` doubled to `/ws/ws/system` — now fixed and commented).
- **Usage snapshot cleared on any non-`connected` state** so a reconnect can't render the prior session's totals as live; panel falls back to HTTP polling summary during the gap.
- **Intake composer SSE** uses `EventSource` (no custom headers — auth is server-side via session id); a transport-level `error` event loop-reconnects a dead session — guard kept in `use-prompter.ts` (a stale localStorage payload or malformed frame could still surface a phantom draft).
- **Secretary live chat (`use-secretary.ts`)** now mirrors that intake-composer resilience (post-2026-06-30): a transport-level SSE `error` resets the `streaming` spinner + surfaces a notice (no permanent "thinking…" hang), `send` is blocked while a reply is streaming (no mid-reply clobber), and the chat persists to `localStorage` and reconnects on reload (status-alive gated).
- **MegaTask intake card** historically had crash/disappears bugs (list[str] nest, depth ValueError→500); confirm-batch path is the multi-project branch (`project_ids`).
- **Kanban drag = admin status-override** which bypasses the in-band lifecycle validator; `skippedPreconditions` only warns on what the panel can detect (PR/docs/subtasks-terminal) — precision over recall, an empty list does NOT mean the move is safe, only that nothing detectable is skipped.
- ~~`ui-store` exists under both `store/ui-store.ts` and `lib/stores/ui-store.ts`~~ — **FIXED** (536bbb64): `lib/stores/ui-store.ts` was removed and replaced with `scroll-restoration-store.ts`; `store/ui-store.ts` is now the sole canonical location.
- **`proxy.ts` is Next.js 16's renamed `middleware.ts`** — same file-convention contract (default export + `config.matcher`), just relocated/renamed terminology (it never ran in true Edge middleware). A reader searching the repo for `middleware.ts` will find nothing; the gate lives at `src/proxy.ts`.
- **`proxy.ts` fails OPEN, not closed** — a slow/unreachable orchestrator on the `/auth/status` probe (1500ms timeout) is treated as "cloud auth off," so the dashboard stays reachable rather than the CEO getting locked out by a transient backend hiccup. This is the deliberately safe default (off is what every deploy starts on) but means a genuinely-armed deployment with a flaky orchestrator could intermittently skip the login gate.
- **`proxy.ts`'s `/tg` exclusion must be anchored** (`tg(?:/|$)`, not a bare `tg`) — an unanchored `tg` in the negative-lookahead matcher would also skip gating on any unrelated route that merely starts with "tg", not just the Mini App; fixed same-PR (8d727785) alongside the initData far-future rejection.
- **`/tg`'s bootstrap POSTs `initData` to `/telegram/webapp-auth` on every mount**, not just cold loads — there's no client-readable signal (the session cookie is httponly) to know a warm reload already has a valid cookie, so `tg/page.tsx` always re-validates; the route is idempotent (re-mints the same cookie) so this is cheap by design, not an oversight.
- **X Post Queue / Roadmap Review Queue hide when empty**, mirroring the release-proposal + playbook queues — a CEO who doesn't see the card has no signal that the underlying engine is even armed; both need `refetchInterval: 30000` to surface a newly-originated draft/cycle without a manual refresh.
- **Conversations tab activity is A2A-only by design**: `latestPulseTimestamps` (switchboard-utils) derives pulses purely from `a2a.message` frames on `/ws/system`, never from the verb/flow traffic sharing that same stream — a CEO ruling, not an oversight, so don't "fix" the switchboard to also light up on ordinary gateway verbs.
- **A2A reply composer is read-only on a task-less conversation**: the backend's `reply_as_ceo` route 400s exactly when the watched conversation has no `task_id` (A2A sends always ride the gateway `send` path, which requires one) — the panel pre-empts that bounce with an explanatory message instead of letting the POST fail. Conversation `status` does NOT gate the composer; the CEO's reply lands in its own direct thread with the participant, not into the watched conversation.
- **Switchboard "peeked pair" state**: a pair with `conversation_id: null` (never talked) has nothing to select via `?conversation=`, so `a2a-view.tsx` tracks it separately (`peekedPair`) and renders its own empty state — don't conflate this with the ordinary `selectedId` empty-state path when touching the drill-in panel.
- **`?dm=` deep-link target is validated, not trusted**: `A2ANewDmDialog`'s `preselectable` check resolves `initialTarget` against the live roster (`useAgentDefinitions`) AND requires the role pass `EXCLUDE_NON_DM_ROLES` — an unknown agent id, an excluded role (auditor/pr_reviewer/prompter/secretary/CEO), or a still-loading roster opens the dialog un-preselected rather than trusting whatever `?dm=` claims.
- **DM latch is one-shot-with-re-arm, not fire-once-forever**: `A2AViewContent` compares `dmParam` against a `prevDmParam` render-phase snapshot to open the dialog on a NEW value, then an effect strips `dm` from the URL; once stripped, `prevDmParam` resets to `null` so a second, identical `?dm=<same agent>` deep link (re-click the same card's DM button, a re-pasted URL) still re-opens the dialog instead of silently no-op'ing against the stale latch.
- **`a2aApi.createConversation`/`sendCeoMessage` must pass `X-Agent-ID: "ceo"` explicitly** (via axios per-call `headers`) — the backend routes they hit resolve the caller's identity from that raw header rather than a DB lookup, so the client's *default*`CEO_AGENT_ID` (a UUID) would persist as `agent_a`/`from_agent` and break every downstream `"ceo"`-string check (reply-budget gate, reply-composer recipient exclusion, admin pairing). `client.ts`'s interceptor uses `has()`/`set()` (case-insensitive) specifically so this per-call override isn't clobbered — `AxiosHeaders` bracket access is case-sensitive and would have silently lost a lowercase key.
- **`A2ANewDmDialog`'s `AgentSelector` uses `excludeRoles`**, a new prop that drops roles from the roster before grouping (not just filters within a group) — used here to exclude the CEO itself plus every role without `read_a2a` on its manifest (auditor, pr_reviewer, prompter, secretary), since a DM to one of them would be a black hole no one ever reads.
- **Every URL param write forks `ScrollRestoration`'s route key and force-scrolls `<main>` to top** — `WorkSessionsView`'s search/status filters learned this the hard way (a per-keystroke `q=` param bounced the page) and moved to local `useState`; the Git page's own `?tab=` switch is fine since it's a deliberate, infrequent navigation, not per-keystroke. Don't route per-keystroke or high-frequency filter state through `router.replace`/`push` on this page.
- **Workstation's `ProjectsView` filters (q/cell/inactive) are local `useState`, not URL params** — deliberate: any URL write forks `ScrollRestoration`'s route key and force-scrolls `<main>` to top. Trade-off disclosed: a filtered Projects view is no longer a shareable/bookmarkable link (only `?tab=` rides the URL). `/products` and `/projects` are now redirect shims to `/workstation?tab=...`, so old bookmarks/links still resolve.
- **Card-grid sort direction uses a comparator multiplier, not sort-then-reverse** (`sortProducts`/`sortProjects` in `products-view.tsx`/`projects-view.tsx`, mirrors `task-table.tsx`'s pattern) — reversing an already-sorted array also flips the relative order of ties; a first cut of the Workstation card sort got this wrong and was fixed post-adversarial-review (9a33efba, PR #556). `sortProjects`' cell-key comparator also falls back to the raw `assigned_cell` string when it's outside the panel's `teamLabels` map (the backend `Team` enum is a superset — e.g. `fullstack`/`system`) instead of throwing on an unmapped key.
> - `0065ecbb` chat: session task_links in one read — `sessionsApi` drops `getTasksForSession`; `use-channels.ts` (since removed in the comms teardown) `useSession` relies on the single populated response; adds `use-session.test.tsx`.
> - `da563487` Wave 2: A2A live view (#297) — new `app/(dashboard)/a2a/page.tsx` (classic list view + transcript + `A2AReplyComposer`), `hooks/use-a2a-live.ts`, `lib/api/a2a.ts` admin client, `useA2ALiveStream` added to `use-websocket.ts`. Backend pairs with `EventType.A2A_MESSAGE_SENT` + `websocket_bridge._handle_a2a_message_event`.
> - `876e19b3` A2A switchboard (#298) — `page.tsx` gains the switchboard/list view toggle (default switchboard) + `peekedPair` state; new `components/a2a/{a2a-switchboard,a2a-switchboard-utils,a2a-pair-card}.tsx`; `useA2AAdminPairs` added to `use-a2a-live.ts`.
> - `a7147702` feat(panel): full mobile responsiveness pass — touches the A2A page's single-visible-pane layout (`h-dvh`, back affordance) among other routes.
> - `abf4b35f` (2026-07-17, PR #546, "wave-1 quick wins") — notifications page resolves `from_agent` via `getAgentDisplayName` (was `notification.from_agent.slice(0, 8)`, a raw UUID prefix); metrics charts (usage time-series, agent/team usage, model donut) gained a "no data" empty state alongside the existing loading skeleton.
> - `ca07c83f` + `40b1a586` (2026-07-17, PR #546) — scroll-bounce fix: `scroll-restoration.tsx`'s route key now strips UI-only params before comparing (`UI_ONLY_PARAMS=["expanded"]`, exported `buildRouteKey`) so a tasks-page row expand/collapse no longer forks/resets the saved scroll position; new floating `ScrollJumpButtons` (`components/scroll-jump-buttons.tsx`, mounted as a `<main>` sibling in `(dashboard)/layout.tsx`) re-observes `<main>`'s children via `MutationObserver` across a Suspense fallback→content swap so the `ResizeObserver` never watches a detached fallback node; the dead, unfiltered duplicate `hooks/use-scroll-restoration.ts` was deleted; `agent-utils.ts` `AGENT_NAMES` gains `system: "System"` for backend-authored notifications/events.
> - `d83104e9` + `9a08cb3e` (2026-07-17, PR #546) — `ai-routing-card.tsx` confirm/toast copy now reads "Role/global routing now on … — per-agent pins kept" (was "All agents now on … Clears any overrides"), matching the backend fix that mode switches no longer wipe the whole `model_assignments` table — see `docs/map/support-services.md`.
> - **Wave 3** (2026-07-17, branch `feature/wave-3-a2a-ceo`, PR #547) — CEO New-DM composer: `a2a-new-dm-dialog.tsx` (opens a fresh CEO-owned 1:1, `AgentSelector`'s new `excludeRoles` prop) + `a2a-direct-composer.tsx` (posts in a CEO-owned thread, no task link needed) wired into `page.tsx`'s composer-selection branch (CEO-owned thread → direct composer; task-linked watched thread → reply composer; else read-only). `use-a2a-live.ts` adds `useCreateCeoConversation`/`useSendCeoMessage`; `lib/api/a2a.ts` adds `createConversation`/`sendCeoMessage` (both force `X-Agent-ID: "ceo"` per-call). `client.ts`'s header injection changed from an unconditional overwrite to a `has()`/`set()` default so a per-call override survives. Backend: `A2AService._maybe_wake_ceo_recipient` wakes an offline `read_a2a`-capable recipient of a CEO DM via the `a2a_request` dispatch path — see `docs/map/a2a-audit-journal-permissions.md`. Same branch also scrubbed "message the CEO" recipes from `docs/rag`/`agents/prompts` (agents are never taught to DM the CEO — reply-only).
> - (open PR #548, branch `feature/wave-2-hygiene-charts`, 2026-07-17) Wave 2 hygiene + charts: `/work-sessions` route now redirects to `/git?tab=sessions` (moved under Git as a "Work Sessions" tab, `git-page.tsx` gains a `Tabs`); `GitActionsPanel` gains a confirm-gated "Clean Up Stale Branches" button (`useCleanupBranches`, cursor-resumable); `WorkSessionsView`'s filters moved from URL params to local state (ScrollRestoration bounce fix); new `SessionTrendChart` / `CostTrendChart` / `SpendTrendChart`.
> - `e16fb634`+`8d727785` (2026-07-18, PR #554, Telegram V3 Mini App) — new `(tg)` route group (`layout.tsx` + `tg/page.tsx`) and `components/tg/{tg-tab-bar,tg-approvals-tab,tg-inbox-tab,tg-board-tab,tg-chat-tab}.tsx`; new `components/tasks/mobile-task-board.tsx` (read-only, grouped-by-status) and `lib/telegram/webapp.ts` (WebApp bridge + `waitForTelegramWebApp` poll); `a2a-new-dm-dialog.tsx` exports `EXCLUDE_NON_DM_ROLES` for `tg-chat-tab.tsx` to reuse; `use-a2a-live.ts`'s `useA2AMessages` gains an optional `refetchInterval` (the cockpit polls instead of using WS); `proxy.ts` matcher excludes `tg(?:/|$)` (anchored in the fix commit — see Gotchas). Backend companion: `POST /api/telegram/webapp-auth` — see `docs/map/api-routes-schemas.md`.
> - `49a87c2b`+`9a33efba` (2026-07-18, PR #556, Workstation card grids) — new `product-card-grid.tsx`/`project-card-grid.tsx` (same intrinsic `grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]` sizing as `agent-grid.tsx`), reusing `CellsList`/`ProgressCell` (newly exported from `product-table.tsx`) and `getExternalUrl`/badge renderers (newly exported from `project-table.tsx`); each Workstation surface gains a Cards\|Table toggle (default Cards, persisted per-surface via `ui-store`'s new `productsView`/`projectsView`) plus a name/cell(-count) sort control that only applies in card view — the table keeps its own unsorted render. `sortProducts`/`sortProjects` (both exported for direct unit tests) use a direction multiplier rather than sort-then-reverse, since reversing also flips the relative order of ties, and `sortProjects`' cell-key comparator falls back to the raw `assigned_cell` value for a backend `Team` outside the panel's `teamLabels` map instead of crashing; the fix commit tightened both to this shape after adversarial review caught the initial sort-then-reverse cut.
> - `97306fe7`+`151d6e0f` (2026-07-18, PR #557, customizable Quick Actions) — the hardcoded `QuickActionsBar` (deleted) is replaced by `QuickActionsCard` (`components/dashboard/quick-actions-card.tsx`), backed by a new 23-action `QUICK_ACTIONS_REGISTRY` (`components/dashboard/quick-actions-registry.ts`, id/label/icon/href/tip per entry, tab-parameterized deep links e.g. `/git?tab=sessions`, `/metrics?tab=delivery`); `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy bar destination (secretary/journals/auditor) so the swap drops nothing from a fresh install's default view. Selection persists as `quickActionIds` on `useUIStore` (new key in `partialize`, contract-tested); `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids instead of crashing. A pencil-icon `QuickActionsCustomizeDialog` (inline in `quick-actions-card.tsx`) lets the CEO show/hide + reorder (up/down arrows) + reset to defaults; the card shows an empty-state message when every action is hidden. `command-center.tsx` and its test swap `QuickActionsBar`/`QuickActionsBarStub` references for `QuickActionsCard`/`QuickActionsCardStub`.
> - `431cb2ae`+`70e53bc1` (2026-07-18, branch `feature/wave-9-agents-hub`, PR #558) — Agents hub: `/agents` gains Fleet + Conversations tabs (`?tab=`, Fleet default). The entire former `/a2a` page body moves into `components/a2a/a2a-view.tsx` (`A2AView`, a pure lift — its own `?conversation=` param keeps working, every writer now targets `/agents` preserving the rest of the query string) and a new `components/agents/agents-fleet-view.tsx` (`AgentsFleetView`) holds the old `/agents` roster body; `a2a/page.tsx` becomes a redirect shim → `/agents?tab=conversations`; the sidebar's standalone A2A nav entry is removed (folded into "Agents"). `AgentCard` gains a DM quick-action button (hidden for `EXCLUDE_NON_DM_ROLES`) pushing `/agents?tab=conversations&dm=<agent id>`; `A2AViewContent` latches the `?dm=` param open once per distinct value and strips it from the URL. Follow-up fix (`70e53bc1`): the deep-linked target was being trusted blindly — `A2ANewDmDialog` now only preselects it once validated against the live roster + `EXCLUDE_NON_DM_ROLES`, and the latch re-arms once `dm` clears so a repeated identical deep link still fires.
> - `cc57b9f1` (2026-07-18, branch `feature/wave-11-journals-tab`) — Journals joins the Agents hub as its third tab: `agents/page.tsx`'s `TabDef.value` gains `"journals"`, rendering `<JournalsView/>` in a third `TabsContent`. The entire former `/journals` page body moves into `components/journals/journals-view.tsx` (`JournalsView`, mirroring the `A2AView` lift — `agent`/`type`/`task` params keep working, every writer now targets `/agents` preserving the rest of the query string); `journals/page.tsx` becomes a redirect shim → `/agents?tab=journals`; `journals/[entryId]/page.tsx`'s three back-links retarget to `/agents?tab=journals` (the detail route itself is unchanged). `sidebar.tsx`'s standalone Journals nav entry is removed (its `BookOpen` icon import dropped) and the "Agents" tip text now reads "...A2A conversations, and journals"; `quick-actions-registry.ts`'s `journals` entry's `href` retargets `/journals` → `/agents?tab=journals` (mirroring the `a2a` entry's wave-9 retarget).
> - `7e01c0ce` (2026-07-18, PR #570, "project-branded drafts + project badges") — the X and video post-queue API responses now carry `project_slug`/`project_name` (backend `api/schemas/project_fields.py` unloaded-guard helper); both `x-post-queue.tsx` and `video-post-queue.tsx` render the new shared `ProjectBadge` (`project-badge.tsx`) next to the existing source-kind badge, so a multi-project CEO can tell drafts apart. Also lands `CompanyGoalsService.resolve_product_name` (project name → charter `company_goals.company_name` → "RoboCo" fallback) so release posts/videos stop hardcoding "RoboCo"; `business/goals-tab.tsx` gains the company-name input backing it.
> - `29b375f1` (2026-07-18, "session links target the owning task") — fixes a dead-link bug: `/work-sessions/<id>` was never a real route. `work-session-card.tsx`'s "View Details" link is deleted outright; `work-session-table.tsx`'s 4 link sites (desktop-table row, table icon-button, mobile-card branch link, mobile icon-button) all retarget `href={`/tasks/${session.task_id}`}` instead, with tooltip copy reworded to "Open the owning task — the session's branch, commits, and PR live on its detail tabs".
> - `fc6d6f64` (2026-07-18, "CEO pairs join the switchboard matrix; sections collapsible") — two independent A2A switchboard fixes. (1) The switchboard's static pair matrix previously filtered by `is_human_only_role` (a spawn-semantics check) which dropped the CEO before `can_a2a_direct` (which explicitly allows CEO→anyone) ever ran, so the matrix carried zero CEO pairs — backend `agents_config.py` now excludes only `prompter`/`secretary`/`system` (matrix size 70→93; a later fix narrows it further, see `docs/map/prompts-roles-taxonomy.md`), and `a2a-switchboard-utils.ts` gains a `"ceo"` entry at the FRONT of `SECTION_ORDER` labeled `"CEO Direct"`. (2) Every switchboard section header becomes a collapse toggle: `a2a-switchboard.tsx` wraps each section in a Radix `Collapsible` (session-local `collapsed` state per `groupKey`, default open), with the `HelpTip` moved onto the inner label span rather than the `CollapsibleTrigger asChild` button — the same asChild-clobbers-`data-state` trap noted elsewhere in this doc's Gotchas (Switch/TabsTrigger).
> - **Telegram Mini App V4** (2026-07-19, PR #576 + #582 + `a072b980` fix, "Today brief, native approvals, live data, bot tier, chat bridges" + "premium Mini App cockpit") — the cockpit gets a 5th default-opening tab and a native-app visual overhaul. `TgTabBar` gains `"today"` as the first tab. New `TgTodayTab` renders off ONE `GET /telegram/today` round trip (TanStack Query, 45s `refetchInterval` + WS-invalidate on any `USAGE_SNAPSHOT` frame): `SpendHero` (cost figure, signed delta-vs-yesterday chip, a 7-day `Sparkline`), a `TgCircleAction` quick-action ring, a `NeedsYouBanner`, a live `TgAvatar` "Fleet" block, and a "Shipped this week" `DayBars` block. New shared primitives in `ui.tsx` (`TgCircleAction`/`TgAvatar`/`TgSection`/`TgRow`/`TgRowIcon`/`TgStat`, all new). `TgApprovalsTab` becomes a genuine native card stack over the new `approvals/` subdirectory, where `PrimaryAction` drives Telegram's `MainButton` via a new `useMainButton` hook and the tab itself wires `useBackButton` for focused-card back nav — both null-safely fall back to a visible button outside Telegram. `useApprovalQueue` normalizes all 4 held-draft sources into one list and surfaces `anyFailed` so a dead queue source is never silently rendered as "queue is clear". `TgChatTab` drops polling for the desktop A2A idiom: `useA2ALiveStream()` invalidates on every `a2a.message` frame with a 10s poll fallback only while `/ws/system` is down. New Telegram-native theme adoption: `webapp.ts` gains theme/button/haptics types + `createDevMockWebApp`/`isDevMockWebApp` (the `/tg?demo=1` dev-browser fallback, `NODE_ENV==="development"`-gated); new `hooks.tsx` + `theme.ts` apply `themeParams` → shadcn CSS vars **scoped to `#tg-shell`** (the desktop dashboard is untouched); `(tg)/layout.tsx` gains the `id="tg-shell"` scoping hook + Share Tech Mono font loading; `globals.css` ships a constant dark-amber `#tg-shell` skin independent of dashboard light/dark, that Telegram's `themeParams` inline-override on top of. `a072b980` (same-day fix) closes a prod-only bug: a plain-browser `/tg` visit gets the WebApp bridge with empty `initData` (the CDN script still loads outside Telegram), which was POSTed and 422'd into "Couldn't sign in" — `tg/page.tsx` now shows the "Open from Telegram" wall for ANY non-dev-mock bridge with empty `initData`. Follow-up polish (`c7605b0d` #582): `charts.tsx` (`Sparkline`/`DayBars`, theme-driven via `currentColor`); `tg-inbox-tab.tsx`/`tg-chat-tab.tsx` adopt `TgAvatar` tokens; `TgRowIcon` gains a `tone` prop so `tg-approvals-tab.tsx`'s `KIND_META` color-codes each queue kind; backend `/telegram/today` gains `spend.series`+`delta_pct`+`velocity` — see `docs/map/notification.md` for the backend side.
> - **"feature/tg-miniapp-v5"** (2026-07-19) — a further visual/interaction polish pass on the V4 cockpit, additive-only (the one relocation: Today's standalone "Approve" button becomes a 4-item ops ring — Approvals is still reachable via the tab bar). New `tg-icons.tsx` (175 lines) — 9 SVG duotone components (`IconToday`/`IconSeal`/`IconInbox`/`IconBoard`/`IconChat`/`IconShip`/`IconAckAll`/`IconSweep`/`IconFleet`), scoped to hero surfaces only (utility chrome stays lucide-react); wired into `tg-tab-bar.tsx` (the 5 tab icons) and `tg-today-tab.tsx` (the new ops ring). New `motion.tsx` (105 lines) — `useCountUp(target, durationMs=650)` (rAF ease-out-cubic, instant under `prefers-reduced-motion`) and `TgSheet` (bottom-sheet dialog wired to Telegram's native `BackButton`, backdrop-tap-to-close, haptic on open); used by the new `tg-task-sheet.tsx` and by `tg-today-tab.tsx`'s Fleet/Sweep sheets. New `tg-task-sheet.tsx` (151 lines) — `TgTaskSheet`: read-only task-detail bottom sheet (status badge, "bounced ×N" chip, ACs, up to 5 open revision findings via `useTaskFindings` with an overflow line, "Open PR #N" link; skips the findings fetch in demo mode); rendered from `tg-board-tab.tsx` alongside `MobileTaskBoard` via a new `onTaskPress` prop. New vendored `ShareTechMono-Regular.woff2` under `app/(tg)/fonts/`, loaded via `next/font/local` as `--font-share-tech` on `#tg-shell`, applied only to the display voice (labels/numerals/wordmark) — never body text. `tg-today-tab.tsx` (largest diff) replaces the old single "Approve" action with a 4-item ops ring (Ship/Ack-all/Sweep/Fleet): `SpendHero`'s numeral gains count-up, a new `FleetSheet` shows the full roster (opened by tapping the truncated 3-agent preview), `runAckAll` bulk-acks via the existing notifications API, `runSweep` calls the existing `gitApi.cleanupBranches` per has-token project (no new backend surface), plus a blinking-cursor "ROBOCO_" wordmark header. `demo-data.ts` gains `DEMO_TASKS`/`DEMO_NOTIFICATIONS` fixtures.
> - **"panel-perf-p3-p4"** (2026-07-19) — kanban + task-table + scorecards perf pass, paired with the backend N+1 fix in `docs/map/metrics-observability.md`. `kanban-board.tsx`: `tasksByStatus` grouping wrapped in `useMemo`, `handleAction` stabilized via `useCallback` + a `tasksRef` so child memoization actually holds. `kanban-card.tsx`: wrapped in `React.memo`. `kanban-column.tsx`: genuine windowing via the new `@tanstack/react-virtual` dependency — `useVirtualizer` (132px estimated card height, overscan 6) replaces a plain `.map()`, only visible virtual rows mount, absolute-positioned + `measureElement`'d for real-height correction; the column itself stays the dnd-kit droppable target so windowing doesn't break drag targeting. `task-table.tsx` (673 changed lines, no virtualization): row/card JSX extracted into top-level `memo`-wrapped `TaskTableRow`/`TaskTableCard`, fed stable props (`toggleExpand` via `useCallback`, lookup maps passed through) — pagination (pre-existing) is the actual bound on rendered rows, not windowing. `scorecards-tab.tsx`: `MemberRow` no longer self-fetches — `ScorecardsTabContent` calls the new `useAllMemberScorecards()` once and passes each row its slice via a `useMemo`'d Map; a table-level "Failed to load member scorecards" banner replaces the old per-row failure cell.
> - **"Panel consistency & UX wave"** (#694/#688, 2026-07-24) — five streams. Stream1: project/task dialog field-sync audit (`docs/frontend/forms/forms-audit.md`, `project-fields-audit.md`) plus settings-page save-feedback toasts. Stream2: the Cmd+K **command palette** (`command-palette.tsx` + `use-command-palette.ts`, fuzzy-match + localStorage recents) wired into the header search box. Stream3: the Tasks page gains **List\|Kanban tabs** sharing URL filter state, and a follow-up (#679) redirects the standalone `/kanban` route here, drops its sidebar entry, and swaps the bottom tab bar's kanban slot to Agents. Stream4: a responsiveness/touch-target sweep — `Button size="sm"` 32px→36px, `AlertDialog` gains the same scroll-at-short-viewport pattern `Dialog` already had, the Scorecards Members table wrapped in `ResponsiveTable`, and (after `sync_branch`'s rebase reset silently dropped the fix twice — see `docs/map/worksession-git.md` #683) every remaining sub-36px icon button across 8 files bumped to 36px. A closing pass (`73275ff0`) corrects the task-budget help text for the removed `TaskType`-default fallback (#666) and moves the forms-audit docs into the governed `docs/frontend/forms/` tree.
> - **Project settings full page** (`0e97fe6a`, Wave C-2, 2026-07-24) — the ~30-field, 7-tab `edit-project-dialog.tsx` (1061 lines) is deleted outright. Settings now live at `/projects/[id]/settings`: a card-per-concern grid (`components/projects/settings/*`, one card per concern — identity, git auth, placement, environments, CI/CD commands, sandbox, budget/ops) with per-card save, Conventions promoted to a page-level tab at natural width, and a new slim `quick-edit-project-dialog.tsx` (name/cell/active) for the fast path. The project list's Edit action now routes to the settings page instead of opening a dialog.
> - **Dialog standardization** (`581f0320`, Wave C-2, 2026-07-24) — no behavioral change: `collapsible-section.tsx` moves from `tasks/task-detail/` to `ui/` as the single sectioned-disclosure primitive (task dialogs' raw `Collapsible` and create-project's ad-hoc `showAdvanced` both converge onto it); every hand-rolled dialog footer across the panel becomes `DialogFooter`; dialog widths collapse from ten ad-hoc classes to three named sizes, deliberate outliers annotated inline.
| Release-proposal silent hide on non-404 error | `components/dashboard/release-proposal-card.tsx:120` | A genuine 500/network error from `/api/release/proposal` collapsing onto `!proposal` would hide the card silently; the fix surfaces an error+retry card, but `releaseApi.getProposal` must keep mapping only 404→null — any other null-return path reintroduces the blind spot | High |
| Approve dialog notes-required labeling | `components/dashboard/release-proposal-card.tsx:60` | Approve path needs no notes (executor is fail-closed), reject requires ≥10 chars; if the shared `RequiredNotesDialog` is ever reused for approve it would mislabel the CEO action as requiring a reason | Medium |
| Stale usage snapshot on `/ws/system` leave | `hooks/use-rate-limit-websocket.ts:71` | Snapshot is cleared on `state !== "connected"` but only via this single effect; if a future second subscriber or an unmount-without-state-change path skips the effect, the prior session's totals/cost could render as live | Medium |
| Kanban admin-override drag skips lifecycle | `components/kanban/core/kanban-board.tsx:160` | A confirmed override routes through `useUpdateTask` (admin status-override), bypassing the in-band validator; `skippedPreconditions` is precision-over-recall — an empty list does not guarantee safety, and a careless confirm can still complete a task with no PR / QA-bypass / docs-incomplete | High |
| Intake composer SSE stuck | `hooks/use-prompter.ts:678` | `EventSource` auto-reconnects on transport error; the guard stops loop-reconnecting a dead session, but a server that drops without closing the SSE leaves `isSending=true` until `turn_end` arrives — the composer can appear stuck mid-send | Medium |
| Panel token on live-chat bridges | `lib/api/prompter-live.ts:89` | `EventSource` cannot send custom headers, so the live-intake SSE carries no `X-Agent-*` auth headers — auth relies entirely on the session id being unguessable; any session-id leakage grants stream access | Medium |
| ~~Duplicate `ui-store` paths~~**FIXED** | `store/ui-store.ts` vs `lib/stores/ui-store.ts` | RESOLVED (536bbb64): `lib/stores/ui-store.ts` removed; `lib/stores/` now exports `scroll-restoration-store` only; `store/ui-store.ts` is sole canonical | Low |
| Single-commit 141-gaps fill-in blast radius | `15effce0` (#283) | All panel regression-relevant fixes landed in one commit; a partial revert to fix one surface can drop the others (release error card, kanban prompt, usage clear, `/ws/system` path fix) | High |
## Health
The panel is a mature, well-structured Next.js 16 App-Router app: clean domain folders, TanStack Query for server state, zustand for client state, a single shared `/ws/system` subscriber, and recent hardening around the high-stakes CEO surfaces (release proposal, playbook queue, kanban override, usage snapshot). The chief fragility is concentration: nearly every logic-relevant fix since the baseline landed in one 141-gaps fill-in commit, so any partial revert risks re-opening several independent blinds spots at once. The `ui-store` duplicate was resolved (536bbb64); the remaining standing hygiene item is the kanban admin-override bypass (no-PR / QA-skip / docs-incomplete moves can still be confirmed).