Skip to content

feat: web dashboard core infrastructure (PR 1 of 2)#344

Closed
Aureliolo wants to merge 13 commits intomainfrom
feat/web-dashboard
Closed

feat: web dashboard core infrastructure (PR 1 of 2)#344
Aureliolo wants to merge 13 commits intomainfrom
feat/web-dashboard

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

Core infrastructure for the Vue 3 + PrimeVue + Tailwind CSS web dashboard (PR 1 of 2). This PR establishes the foundation; page views and feature components follow in PR 2 (feat/web-dashboard-pages).

  • API layer: Axios client with JWT interceptor, response envelope unwrapping, encodeURIComponent on all dynamic path segments; endpoint modules for auth, agents, tasks, budget, approvals, messages, analytics, company, providers, health
  • State management: 10 Pinia stores (auth, agents, tasks, budget, messages, approvals, websocket, analytics, company, providers) with WebSocket real-time event handling
  • Composables: useAuth (token lifecycle), usePolling (interval management), useOptimisticUpdate (rollback on failure)
  • Router: Vue Router with auth guards (login/setup/placeholder home), lazy-loaded routes
  • Types: Full TypeScript type definitions mirroring backend Pydantic models (verified against source)
  • Components: Common (ErrorBoundary, LoadingSkeleton, PageHeader, EmptyState, StatusBadge) and Layout (AppShell, Sidebar, Topbar, ConnectionStatus)
  • Auth views: LoginPage, SetupPage
  • Utils: Constants, error helpers (status-code mapping), formatters
  • Styles: Tailwind CSS v4, PrimeVue Aura dark theme preset
  • Config: Vite, TypeScript (strict), ESLint, Vitest, env.d.ts
  • Docker: nginx config for SPA routing + API/WS proxy, multi-stage Dockerfile
  • CI: Dashboard lint, type-check, test, build, and audit jobs gating ci-pass
  • Tests: 165 unit tests across 17 test files (stores, composables, utils, components, router guards, API client)

Review history

This code went through 3 rounds of local agent review (code-reviewer, type-design-analyzer, silent-failure-hunter, pr-test-analyzer, comment-analyzer, security-reviewer) plus 3 rounds of external reviewer feedback (CodeRabbit, Copilot, Gemini, Greptile, Qodo, Ellipsis). 150+ findings addressed across all rounds. Supersedes #342 (closed for fresh reviews).

Test plan

  • npm --prefix web run lint — ESLint passes (0 errors)
  • npm --prefix web run type-check — vue-tsc passes
  • npm --prefix web run build — Vite production build succeeds
  • npm --prefix web run test — 165 tests pass
  • CI dashboard-lint, dashboard-type-check, dashboard-test, dashboard-build, dashboard-audit all green

Part of #233 (page views in PR 2: #340)

🤖 Generated with Claude Code

Replace placeholder Coming Soon page with production-ready SPA dashboard.

- Vue 3.5 + Vite + TypeScript, Pinia stores, PrimeVue unstyled + Tailwind
- 13 views: Dashboard, Tasks (Kanban+List), Approvals, Agents, Budget,
  Messages, Org Chart, Settings, Login/Setup, stub pages
- Real-time WebSocket integration with exponential backoff reconnect
- ECharts spending charts, vue-flow org chart, drag-and-drop Kanban
- API client with JWT interceptor and envelope unwrapping
- 77 tests across 16 test files (stores, utils, components, API client)
- Fix nginx WebSocket proxy path (/ws -> /api/v1/ws), update CSP
- Multi-stage Docker build (Node builder -> nginx runtime)
- CI: dashboard-lint + dashboard-test jobs added to ci-pass gate
…r handling

Pre-reviewed by 5 agents (code-reviewer, python-reviewer, pr-test-analyzer,
silent-failure-hunter, security-reviewer), 47 findings addressed.

Key changes:
- Align all TypeScript interfaces with backend Pydantic models (AgentConfig,
  Task, CostRecord, BudgetConfig, PersonalityConfig)
- Add token expiry persistence, client-side rate limiting on login/setup
- Fix WebSocket reconnection (pending subscriptions queue, max retries)
- Fix Kanban drag-and-drop (@EnD@add on receiving column)
- Add global error handler, unhandled rejection catcher
- Add eslint-plugin-security, HSTS header, remove plaintext ws: from CSP
- Fix auth timer leak, budget store error handling, WS cleanup on unmount
- Update docs (CLAUDE.md, README, roadmap, design spec, user guide)
Fixes from code-reviewer, silent-failure-hunter, comment-analyzer,
type-design-analyzer, docs-consistency, and external reviewers
(CodeRabbit, Copilot, Gemini, CodeQL).

Key changes:
- Align types with backend (ToolAccessLevel, decision_reason, optional fields)
- Harden WebSocket store (race condition, log sanitization, reconnect state)
- Consistent error handling via getErrorMessage() across all stores
- Fix optimistic update rollback and polling unmount safety
- Add keyboard accessibility to TaskCard and ApprovalCard
- Fix auth guard to use route meta instead of hardcoded paths
- Fix sidebar route matching prefix collision
- Add WS memory caps (500 records/messages)
- Prevent form submission bypass in TaskCreateDialog
- Disconnect WebSocket on logout
- Gate global error handlers to DEV mode
- Fix CSP connect-src for WebSocket protocols
- Update CLAUDE.md package structure and commands
- Update docs (getting_started, user_guide) for web dashboard
- Remove dead useWebSocket composable
- Fix SpendingSummary sort order
…sholds

CI Dashboard Test job was broken — it ran `npm test -- --coverage` but
@vitest/coverage-v8 was never installed. Added the dependency and removed
the 80% coverage thresholds since the dashboard is new (~15% coverage).
Thresholds can be reintroduced incrementally as test coverage grows.
Stores:
- Fix tasksByStatus O(n²) spread → use push for O(n)
- Schedule auth token expiry timer on page restore
- Fix agent total counter drift on duplicate fired events
- Clear error before approve/reject/fetchConfig/fetchDepartments
- Filter WS messages by active channel

Views:
- Make agentNames a computed (was stale ref)
- Fix SettingsPage loading stuck on fetch failure
- Fix OrgChart retryFetch unhandled promise, use departmentsLoading
- Pre-index agents in OrgChart for O(1) lookup
- Encode agent names in URL path segments
- BudgetPanel retry fetches both config and records
- Gate DashboardPage console.error to DEV

Components:
- Remove lazy pagination from TaskListView (client-side)
- Add keyboard accessibility to AgentCard (role, tabindex, space)
- Add space key handler to TaskCard and ApprovalCard
- Add empty tools state in AgentMetrics
- Guard SpendingChart tooltip for empty params
- Smart auto-scroll in MessageList (only if near bottom)
- Add role="alert" to ErrorBoundary
- Wrap Topbar logout in try-catch
- Use replaceAll for status underscores in TaskDetailPanel

API:
- Encode all dynamic path segments (agents, approvals, budget, providers)

Utils:
- Validate data.error is string at runtime in getErrorMessage
- Handle future dates in formatRelativeTime
- Add Firefox scrollbar support (scrollbar-width/scrollbar-color)

Tests:
- Fix formatRelativeTime test flakiness (use fixed offset)
- Add future date and negative currency test cases
Security:
- Add encodeURIComponent on all taskId and department name path segments
- Fix type mismatches: ProviderConfig, ProviderModelConfig, Channel,
  Message, ApprovalItem now match backend Pydantic models
- Add missing Message fields (to, type, priority, attachments, metadata)
- Remove phantom fields (ProviderConfig.name/enabled, ApprovalItem.ttl_seconds)
- Use literal union for CostRecord.call_category

Error handling:
- Remove empty catch block in OrgChartPage (violates project rules)
- Always log errors in production (DashboardPage, main.ts)
- Use getErrorMessage in AgentDetailPage instead of generic string
- Show agent fetch error in TaskBoardPage ErrorBoundary
- Add ErrorBoundary to SettingsPage for company/provider errors
- Handle fetchUser failure in login by clearing half-auth state
- Redirect to login on token expiry
- Add validation on WebSocket subscription ack data
- Add try/catch on WebSocket send for race condition
- Await fire-and-forget fetches in ApprovalQueuePage and MessageFeedPage
- Add 422/429 error message handling

Performance:
- Move agentIndex Map creation outside inner loop in OrgChartPage

Tests (79 → 131):
- Rewrite useOptimisticUpdate tests to test actual composable
- Rewrite usePolling tests to test actual composable
- Add auth store async tests (login, setup, fetchUser, changePassword)
- Add auth guard tests (4 scenarios)
- Add task store CRUD tests (fetchTasks, createTask, updateTask, etc.)
- Add approval store approve/reject tests
- Add WebSocket store tests
- Add 422/429 error message tests
Remove page views and feature components to separate branch
(feat/web-dashboard-pages) for independent review. Core PR retains:
- API client, types, endpoint modules
- Pinia stores (auth, agents, tasks, budget, messages, approvals, websocket, analytics, company, providers)
- Composables (useAuth, usePolling, useOptimisticUpdate)
- Common/layout components, auth views (Login, Setup)
- Router with auth guards (placeholder home route)
- Utils, styles, all project config
- 128 unit tests across 17 test files
- CI: add dashboard-build and dashboard-audit (npm audit) jobs
- Fix: add @types/node and tsconfig.node.json types for build
Fixes from Gemini, Greptile, Copilot, GitHub Advanced Security:

Bug fixes:
- auth store: setup() now calls fetchUser() instead of constructing
  stale user object with empty id and hardcoded role
- messages store: total counter only increments for messages matching
  activeChannel filter, preventing total/visible count divergence
- tasks store: WS task.created events skip append when filters are
  active, preventing off-filter tasks from appearing in filtered views
- websocket store: pending subscriptions deduplicated to prevent
  duplicate subscribe messages on reconnect
- websocket store: active subscriptions tracked and auto-re-subscribed
  on reconnect to maintain real-time updates after transient disconnect

Security:
- websocket store: sanitize user-provided values in log output to
  prevent log injection (newline stripping)
- nginx CSP: remove blanket ws:/wss: from connect-src, use 'self' only
  (same-origin WS via nginx proxy; CSP Level 3 covers ws/wss)
- nginx CSP: document why style-src 'unsafe-inline' is required
  (PrimeVue injects dynamic inline styles)
- Dockerfile: pin node:22-alpine by digest for reproducible builds
- Dockerfile: run builder stage as non-root user for defense-in-depth

Docs:
- roadmap: change web dashboard status from "implemented" to
  "in progress" (PR 1 of 2)
- README: update status to reflect dashboard foundation merged,
  pages pending

Tests (134 total, +6 new):
- websocket tests rewritten with proper assertions: event dispatch via
  onmessage, wildcard handlers, malformed JSON, subscription ack,
  reconnect exhaustion (drives 20 failed attempts), auto-re-subscribe,
  log sanitization, send failure queuing
- auth tests: setup() now tests fetchUser() call and failure path
- messages tests: verify total not incremented for filtered messages
- tasks tests: verify WS events skipped when filters active
Implements all valid findings from CodeRabbit, Copilot, Gemini, Greptile,
Qodo, and Ellipsis across 38 files:

- Fix PaginatedResponse error branch typing (data: null, pagination: null)
- SPA-friendly 401 handling (dynamic router import vs window.location)
- setTimeout-based polling to prevent overlapping async calls
- Fetch generation counter to prevent stale concurrent overwrites
- Stable JSON serialization for WS subscription dedup keys
- WebSocket unsubscribe removes from activeSubscriptions
- Silent auth clearing detection in setup/login flows
- Broader control character stripping for log injection prevention
- Provider secret stripping (api_key removed from frontend state)
- NaN guards in date formatters, error message sanitization
- Accessibility: aria-hidden, role="alert", aria-label on nav
- HMR timer cleanup, favicon, meta description, nginx WS timeouts
- Split company store errors, explicit return types on endpoints
- 23 new/updated tests covering all behavioral changes
…ent extraction

- Split CI dashboard-lint into separate lint and type-check jobs
- Add generic type params to all apiClient.get/post calls for type safety
- Extract PlaceholderHome to its own SFC, simplify router imports
- Fix StatusBadge type-safe indexing with proper key casts
- Fix LoginPage to use auth.mustChangePassword (computed) not result field
- Add ESLint multi-word exemptions for Sidebar.vue and Topbar.vue
- Add error path tests for transitionTask and cancelTask
- Add StatusBadge color class tests and unknown value fallback test
- Add EmptyState action slot rendering tests
- Add usePolling boundary validation tests (NaN, Infinity, below min)
- Expand budget store tests (fetchConfig, fetchRecords, fetchAgentSpending, invalid payload)
- Add client interceptor tests for JWT attachment
- Fix format tests for future date and round-hour uptime behavior changes
- Fix messages test for WS total no-increment behavior
- Fix useOptimisticUpdate test for sanitized rollback error logging
- Remove unused mockGetTask from tasks test
…on, JSDoc clarity

- Remove overly-broad ws:/wss: from CSP connect-src; 'self' covers
  same-origin WebSocket in modern browsers (Chrome 73+, Firefox 45+)
- Remove redundant router.push('/login') in Topbar logout — clearAuth()
  already navigates, so the explicit push fired guards twice
- Update useOptimisticUpdate JSDoc to document the pending-guard null
  return case (no error set) vs server-error null return (error set)
- Reset reconnectAttempts in disconnect() to prevent stale budget
- Guard setToken against non-positive expiresIn
- Remove HSTS preload (should be opt-in for self-hosted deployments)
- Normalize error in changePassword instead of misleading re-throw
Copilot AI review requested due to automatic review settings March 13, 2026 09:56
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Web dashboard foundation: Vue 3 SPA with auth, layout (sidebar/topbar), real-time status & WebSocket support, core pages (login, setup, loading home) and UI components for tasks, approvals, agents, budget, messages.
  • Documentation
    • Updated README, getting-started, design, roadmap, and user guide to include the web dashboard.
  • Chores
    • CI extended with dashboard lint/type/test/build/audit jobs; Docker adjusted for multi-stage frontend build; ignore patterns and project config added.

Walkthrough

Adds a complete Vue 3 web dashboard (Vite + PrimeVue + Tailwind) with TypeScript, Pinia stores, router guards, an Axios API client, WebSocket store, components/pages, extensive unit tests, Docker/nginx build/runtime updates, CI jobs, and documentation updates.

Changes

Cohort / File(s) Summary
Project config & toolchain
web/package.json, web/tsconfig.json, web/tsconfig.node.json, web/vite.config.ts, web/vitest.config.ts, web/eslint.config.js
Adds frontend project configuration: dependencies, TS configs, Vite, Vitest, and ESLint settings.
Docker / CI / ignore
.dockerignore, .gitignore, docker/web/Dockerfile, .github/workflows/ci.yml
Multi-stage Dockerfile for web, updated ignore patterns, and new CI jobs for dashboard lint/type-check/test/build/audit; ci-pass now depends on these jobs and records their results.
Runtime & static assets
web/index.html, web/.env.example, web/nginx.conf, web/style.css
Replaces placeholder HTML, adds Vite env example, updates nginx with WebSocket proxy and CSP/HSTS, and removes legacy stylesheet.
App entry & UI preset
web/src/main.ts, web/src/App.vue, web/src/primevue-preset.ts, web/env.d.ts
Bootstraps Vue app with Pinia, Router, PrimeVue preset, global error handlers, and TS declarations for Vue SFCs and env.
Styling & theme
web/src/styles/global.css, web/src/styles/theme.ts
Adds Tailwind imports, CSS variables, scrollbar styles, and exported color/token mappings for statuses/priorities/risks.
API client & types
web/src/api/client.ts, web/src/api/types.ts, web/src/api/endpoints/*
Introduces Axios apiClient with interceptors and unwrap helpers; comprehensive API type definitions and typed endpoint wrappers for agents, auth, tasks, approvals, budget, messages, providers, analytics, health, company, etc.
State management (Pinia stores)
web/src/stores/*
Adds stores: auth, websocket, agents, tasks, approvals, budget, analytics, messages, company, providers, and UI-related stores with async actions, WS handling, and public APIs.
Router & guards
web/src/router/index.ts, web/src/router/guards.ts
Vue Router setup with lazy routes (/login, /setup, /) and authGuard enforcing access rules and redirects.
Composable utilities
web/src/composables/*, web/src/utils/*
Adds useAuth, usePolling, useOptimisticUpdate and utility modules for constants, formatting, and error message extraction (isAxiosError/getErrorMessage).
Components & layout
web/src/components/common/*, web/src/components/layout/*
New common components (EmptyState, ErrorBoundary, LoadingSkeleton, PageHeader, StatusBadge) and layout components (AppShell, Sidebar, Topbar, ConnectionStatus).
Pages & views
web/src/views/*, web/src/components/layout/*
Login and Setup pages, PlaceholderHome, and ConnectionStatus integration in layout.
Tests
web/src/__tests__/**
Extensive unit tests across api client, components, composables, router guards, stores, and utils including WebSocket and optimistic update scenarios.
Docs & metadata
CLAUDE.md, README.md, docs/*
Documentation and README updated to reflect merged Web dashboard, development commands, and roadmap status.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant Router
    participant AuthStore as Auth Store
    participant API as Backend API
    participant WS as WebSocket (server)
    participant TaskStore as Task Store

    User->>Browser: Open /login
    Browser->>Router: load LoginPage
    User->>Browser: submit credentials
    Browser->>AuthStore: login(credentials)
    AuthStore->>API: POST /auth/login
    API-->>AuthStore: TokenResponse
    AuthStore->>Browser: store token (localStorage)
    AuthStore->>API: GET /auth/me
    API-->>AuthStore: UserInfo
    AuthStore-->>Router: navigate to /
    Browser->>WS: connect(token)
    WS-->>Browser: open connection
    Browser->>TaskStore: fetchTasks()
    TaskStore->>API: GET /tasks
    API-->>TaskStore: PaginatedResponse
    WS-->>Browser: event task.created
    Browser->>TaskStore: handleWsEvent(event)
    TaskStore-->>Browser: update UI
Loading
sequenceDiagram
    participant Component
    participant Composable as useOptimisticUpdate
    participant LocalState
    participant API as ServerAction

    Component->>Composable: execute(applyOptimistic, serverAction)
    Composable->>LocalState: applyOptimistic() → rollbackFn
    LocalState-->>Component: optimistic UI updated
    Composable->>API: await serverAction()
    alt success
        API-->>Composable: result
        Composable-->>Component: return result, pending=false
    else failure
        API-->>Composable: error
        Composable->>LocalState: rollbackFn()
        LocalState-->>Composable: state restored
        Composable-->>Component: return null, error set, pending=false
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/web-dashboard
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/web-dashboard

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

Dependency Review

The following issues were found:

  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 2 package(s) with unknown licenses.
  • ⚠️ 51 packages with OpenSSF Scorecard issues.

View full job summary

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 13, 2026 09:57 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

Preview Deployment

URL: https://pr-344.synthorg-pr-preview.pages.dev

Built from commit be9cce7

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request lays the essential groundwork for the new web dashboard, providing a modern and robust frontend stack built with Vue 3, PrimeVue, and Tailwind CSS. It establishes the core architectural components, including API communication, state management, routing, and a comprehensive testing suite, enabling real-time monitoring and management of the synthetic organization. This foundational work is the first step towards a fully interactive and responsive user interface.

Highlights

  • Web Dashboard Core Infrastructure: Established the foundational web dashboard using Vue 3, PrimeVue, and Tailwind CSS, setting up the core structure for future page views and feature components.
  • Comprehensive API Layer: Implemented an Axios client with JWT interceptors, response envelope unwrapping, dynamic path segment encoding, and dedicated endpoint modules for various backend services (auth, agents, tasks, budget, approvals, messages, analytics, company, providers, health).
  • Robust State Management: Integrated 10 Pinia stores (auth, agents, tasks, budget, messages, approvals, websocket, analytics, company, providers) with real-time WebSocket event handling for dynamic data updates.
  • Reusable Composables: Developed key Vue composables including useAuth for token lifecycle management, usePolling for interval-based data fetching, and useOptimisticUpdate for UI responsiveness with rollback capabilities.
  • Secure Routing and Authentication: Configured Vue Router with authentication guards for login, setup, and protected routes, along with lazy-loaded routes for optimized performance.
  • Full TypeScript Integration: Ensured full TypeScript type definitions across the frontend, mirroring backend Pydantic models for strong type safety and developer experience.
  • Docker and CI/CD Enhancements: Updated Docker configuration for the web service to use a multi-stage build with nginx for SPA routing and API/WebSocket proxying, and introduced dedicated CI jobs for linting, type-checking, testing, building, and auditing the dashboard.
  • Extensive Unit Testing: Added 165 unit tests across 17 test files, covering critical components, composables, utility functions, router guards, and API client logic to ensure reliability and maintainability.
Changelog
  • .dockerignore
    • Added web dashboard build artifacts to be ignored by Docker.
  • .gitignore
    • Added web dashboard coverage and build artifacts to be ignored by Git.
  • CLAUDE.md
    • Updated project layout description to include the new web/ directory.
    • Added web dashboard development commands to the 'Shell Usage' section.
    • Updated Docker web service description to reflect Vue 3 SPA integration.
    • Expanded the web/ directory structure with descriptions for its subdirectories.
    • Added web dashboard dependencies and Node.js version requirement.
  • README.md
    • Updated the project status to reflect the web dashboard foundation being merged.
  • docker/web/Dockerfile
    • Converted the Dockerfile to a multi-stage build process for the Vue dashboard.
    • Updated the COPY command to use build artifacts from the builder stage.
  • docs/design/operations.md
    • Updated the Web UI status from 'Planned' to 'In Progress' in the design documentation.
    • Clarified the Web UI technology stack as Vue 3 + PrimeVue + Tailwind CSS.
  • docs/getting_started.md
    • Added a new section detailing web dashboard development setup and commands.
  • docs/roadmap/index.md
    • Moved the 'Web dashboard' entry from 'Remaining Work' to 'In Progress' in the roadmap.
  • docs/user_guide.md
    • Updated the web dashboard status from 'Work in Progress' to 'Active Development' in the user guide.
  • web/.env.example
    • Added an example environment variable for VITE_API_BASE_URL.
  • web/app.js
    • Removed the old JavaScript file, replaced by the new Vue application structure.
  • web/env.d.ts
    • Added TypeScript declaration file for Vite environment variables and Vue components.
  • web/eslint.config.js
    • Added ESLint configuration for Vue and TypeScript, including security and Vue-specific rules.
  • web/index.html
    • Replaced the placeholder HTML with the new Vue application root and updated metadata.
  • web/nginx.conf
    • Updated Nginx configuration to include Strict-Transport-Security header.
    • Adjusted Content-Security-Policy to allow inline styles for PrimeVue.
    • Reordered and refined WebSocket and API proxy configurations.
  • web/package.json
    • Added Node.js project configuration with scripts for development, build, lint, type-check, and test.
    • Included dependencies for Vue 3, PrimeVue, Tailwind CSS, Pinia, Axios, Vue Router, and various development tools.
  • web/public/favicon.svg
    • Added a new favicon for the web dashboard.
  • web/src/App.vue
    • Added the root Vue component, integrating RouterView, Toast, and ConfirmDialog.
  • web/src/tests/api/client.test.ts
    • Added unit tests for the API client's request interceptor and response unwrapping utilities.
  • web/src/tests/components/EmptyState.test.ts
    • Added unit tests for the EmptyState component.
  • web/src/tests/components/PageHeader.test.ts
    • Added unit tests for the PageHeader component.
  • web/src/tests/components/StatusBadge.test.ts
    • Added unit tests for the StatusBadge component.
  • web/src/tests/composables/useOptimisticUpdate.test.ts
    • Added unit tests for the useOptimisticUpdate composable, covering optimistic state and rollback logic.
  • web/src/tests/composables/usePolling.test.ts
    • Added unit tests for the usePolling composable, verifying interval calls and error handling.
  • web/src/tests/router/guards.test.ts
    • Added unit tests for the authGuard navigation guard, covering redirection logic.
  • web/src/tests/stores/agents.test.ts
    • Added unit tests for the agents Pinia store, including WebSocket event handling.
  • web/src/tests/stores/approvals.test.ts
    • Added unit tests for the approvals Pinia store, covering API calls and WebSocket event handling.
  • web/src/tests/stores/auth.test.ts
    • Added unit tests for the auth Pinia store, covering login, setup, token management, and user fetching.
  • web/src/tests/stores/budget.test.ts
    • Added unit tests for the budget Pinia store, covering API calls and WebSocket event handling.
  • web/src/tests/stores/messages.test.ts
    • Added unit tests for the messages Pinia store, including WebSocket event handling and channel management.
  • web/src/tests/stores/tasks.test.ts
    • Added unit tests for the tasks Pinia store, covering API calls and WebSocket event handling.
  • web/src/tests/stores/websocket.test.ts
    • Added extensive unit tests for the WebSocket Pinia store, covering connection, reconnection, subscriptions, and event dispatching.
  • web/src/tests/utils/constants.test.ts
    • Added unit tests for application constants.
  • web/src/tests/utils/errors.test.ts
    • Added unit tests for error utility functions, including Axios error handling.
  • web/src/tests/utils/format.test.ts
    • Added unit tests for formatting utility functions (date, currency, number, uptime, label).
  • web/src/api/client.ts
    • Added Axios client setup with JWT interceptor and API response unwrapping utilities.
  • web/src/api/endpoints/agents.ts
    • Added API endpoint functions for agents, including listing, getting, and managing autonomy levels.
  • web/src/api/endpoints/analytics.ts
    • Added API endpoint function for fetching overview metrics.
  • web/src/api/endpoints/approvals.ts
    • Added API endpoint functions for approvals, including listing, getting, creating, approving, and rejecting.
  • web/src/api/endpoints/auth.ts
    • Added API endpoint functions for authentication, including setup, login, change password, and getting user info.
  • web/src/api/endpoints/budget.ts
    • Added API endpoint functions for budget, including getting config, listing cost records, and fetching agent spending.
  • web/src/api/endpoints/company.ts
    • Added API endpoint functions for company configuration and departments.
  • web/src/api/endpoints/health.ts
    • Added API endpoint function for fetching health status.
  • web/src/api/endpoints/messages.ts
    • Added API endpoint functions for messages, including listing messages and channels.
  • web/src/api/endpoints/providers.ts
    • Added API endpoint functions for providers, including listing, getting, and fetching models, with secret stripping.
  • web/src/api/endpoints/tasks.ts
    • Added API endpoint functions for tasks, including listing, getting, creating, updating, transitioning, canceling, and deleting.
  • web/src/api/types.ts
    • Added comprehensive TypeScript interfaces mirroring backend Pydantic DTOs and domain models.
  • web/src/components/common/EmptyState.vue
    • Added a reusable Vue component for displaying empty states.
  • web/src/components/common/ErrorBoundary.vue
    • Added a reusable Vue component for displaying errors with a retry option.
  • web/src/components/common/LoadingSkeleton.vue
    • Added a reusable Vue component for displaying loading skeletons.
  • web/src/components/common/PageHeader.vue
    • Added a reusable Vue component for page headers with title, subtitle, and action slots.
  • web/src/components/common/StatusBadge.vue
    • Added a reusable Vue component for displaying status, priority, or risk badges with dynamic coloring.
  • web/src/components/layout/AppShell.vue
    • Added the main application shell component, integrating sidebar and topbar.
  • web/src/components/layout/ConnectionStatus.vue
    • Added a component to display API and WebSocket connection status.
  • web/src/components/layout/Sidebar.vue
    • Added the main sidebar navigation component with collapse functionality.
  • web/src/components/layout/Topbar.vue
    • Added the top navigation bar component with user menu and connection status.
  • web/src/composables/useAuth.ts
    • Added a Vue composable for accessing authentication state and permissions.
  • web/src/composables/useOptimisticUpdate.ts
    • Added a Vue composable for managing optimistic UI updates with rollback on failure.
  • web/src/composables/usePolling.ts
    • Added a Vue composable for polling functions at fixed intervals with cleanup.
  • web/src/main.ts
    • Configured and initialized the Vue application, including Pinia, Vue Router, PrimeVue, and global error handling.
  • web/src/primevue-preset.ts
    • Defined PrimeVue theme configuration using Aura preset and dark mode selector.
  • web/src/router/guards.ts
    • Implemented a navigation guard for authentication and redirection logic.
  • web/src/router/index.ts
    • Configured Vue Router with history mode, initial routes for login/setup, and applied the authentication guard.
  • web/src/stores/agents.ts
    • Implemented the Pinia store for managing agent data and handling WebSocket events.
  • web/src/stores/analytics.ts
    • Implemented the Pinia store for managing analytics data.
  • web/src/stores/approvals.ts
    • Implemented the Pinia store for managing approval data and handling WebSocket events.
  • web/src/stores/auth.ts
    • Implemented the Pinia store for authentication, including token management, user info, and login/setup flows.
  • web/src/stores/budget.ts
    • Implemented the Pinia store for managing budget data and handling WebSocket events.
  • web/src/stores/company.ts
    • Implemented the Pinia store for managing company and department data.
  • web/src/stores/messages.ts
    • Implemented the Pinia store for managing message data and handling WebSocket events.
  • web/src/stores/providers.ts
    • Implemented the Pinia store for managing provider data, including secret stripping.
  • web/src/stores/tasks.ts
    • Implemented the Pinia store for managing task data, including filtering and WebSocket event handling.
  • web/src/stores/websocket.ts
    • Implemented the Pinia store for WebSocket connectivity, reconnection logic, and event dispatching.
  • web/src/styles/global.css
    • Added global CSS styles, including Tailwind CSS imports, custom theme variables, and scrollbar styling.
  • web/src/styles/theme.ts
    • Defined theme-related constants, including color palettes and status/priority/risk color mappings.
  • web/src/utils/constants.ts
    • Added application-wide constants for various configurations and enumerations.
  • web/src/utils/errors.ts
    • Added utility functions for consistent error message extraction and handling.
  • web/src/utils/format.ts
    • Added utility functions for formatting dates, currency, numbers, uptime, and labels.
  • web/src/views/LoginPage.vue
    • Added the login page component with form handling, lockout logic, and navigation to setup.
  • web/src/views/PlaceholderHome.vue
    • Added a placeholder home page component.
  • web/src/views/SetupPage.vue
    • Added the initial setup page component for creating the first admin account.
  • web/style.css
    • Removed the old CSS file, replaced by the new Tailwind-based global styles.
  • web/tsconfig.json
    • Added TypeScript configuration for the web project, including paths and type definitions.
  • web/tsconfig.node.json
    • Added TypeScript configuration specifically for Node.js-related files in the web project.
  • web/vite.config.ts
    • Added Vite configuration for the web project, including Vue and Tailwind plugins, alias resolution, and API proxy settings.
  • web/vitest.config.ts
    • Added Vitest configuration for unit testing, including globals, environment, and coverage settings.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/ci.yml
Activity
  • The code underwent 3 rounds of local agent review, addressing feedback from tools like code-reviewer, type-design-analyzer, and security-reviewer.
  • Further refinement involved 3 rounds of external reviewer feedback from various AI assistants (CodeRabbit, Copilot, Gemini, Greptile, Qodo, Ellipsis).
  • Over 150 findings were addressed across all review rounds, indicating thorough iteration and quality improvement.
  • The pull request supersedes a previous attempt (feat: web dashboard core infrastructure (PR 1 of 2) #342), which was closed to allow for fresh reviews on this new foundation.
  • A detailed test plan is provided, including linting, type-checking, building, and unit testing steps, all of which are expected to pass.
  • CI jobs for the dashboard (lint, type-check, test, build, audit) are configured to gate the ci-pass status, ensuring automated quality checks.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR establishes the full Vue 3 + PrimeVue + Tailwind CSS dashboard foundation — API layer, 10 Pinia stores, composables, router, TypeScript types, layout components, auth views, utilities, nginx config, multi-stage Dockerfile, and CI jobs — and is well-structured overall with 165 unit tests, solid type coverage, and good defensive patterns (runtime WS payload validation, stale-request generation tracking, optimistic-update rollback). A few issues need attention before merge:

  • Critical — broken logout on 401: web/src/api/client.ts's response interceptor removes localStorage tokens and calls router.push('/login'), but never clears the Pinia auth store's in-memory token ref. The auth guard reads Pinia state (not localStorage), sees the user as still authenticated, and calls next('/') — aborting the redirect. The user lands back on the dashboard with stale Pinia state; every subsequent API request omits the Authorization header (since localStorage is empty), generating a continuous 401 loop until a hard page refresh. The fix is to lazy-import useAuthStore inside the interceptor callback (same pattern used for the router) and call clearAuth().
  • High — potential infinite loop in fetchDepartments: web/src/stores/company.ts paginates in a while (true) loop that breaks only when allDepts.length >= result.total. If the API ever returns an empty page with a non-zero total (deleted records between pages, pagination bug), the loop runs indefinitely. A one-line result.data.length === 0 guard eliminates the risk.
  • Medium — missing WS payload validation in messages.ts: Every other event-handling store validates the payload structure before casting; messages.ts skips this, risking malformed messages with undefined fields being appended to the list.
  • Minor — shared loading flag in budget.ts: fetchConfig, fetchRecords, and fetchAgentSpending share one loading ref; concurrent calls cause premature loading-indicator resets.

Confidence Score: 3/5

  • Not safe to merge until the 401 interceptor auth-state desync is fixed — users can be silently trapped in an unauthenticated-but-appears-authenticated loop after token expiry.
  • The overall architecture is solid and the 165-test suite shows thorough coverage, but the 401 interceptor bug in client.ts is a functional correctness issue that affects every user whose token expires mid-session — a session management failure that manifests as an invisible broken state rather than a clean logout. The infinite-loop risk in fetchDepartments is a secondary concern that could cause browser hangs under a backend regression. These two issues together pull the score to 3; the style/minor findings are straightforward fixes.
  • web/src/api/client.ts (401 interceptor auth-state desync) and web/src/stores/company.ts (pagination infinite-loop guard) need attention before merge.

Important Files Changed

Filename Overview
web/src/api/client.ts Axios client with JWT interceptor and envelope unwrapping — 401 handler clears localStorage but not the Pinia auth store, causing the auth guard to block navigation to /login and leaving users stuck in a broken authenticated state.
web/src/stores/auth.ts Auth store with token lifecycle, expiry timer, and clearAuth — well-structured; correctly handles 401 from fetchUser, HMR disposal, and timer cleanup on re-login.
web/src/stores/websocket.ts WebSocket store with exponential back-off reconnect, deduplication, and auto-resubscribe on reconnect — JWT-as-query-param risk is acknowledged and mitigated with nginx access_log off.
web/src/stores/company.ts Company store with stale-request generation tracking — fetchDepartments pagination loop has no guard against an empty page with a non-zero total, risking an infinite fetch loop.
web/src/stores/messages.ts Message store with stale-request guards and channel filtering — missing the structural payload validation present in every other WS-event-handling store, which can allow malformed messages to be appended.
web/src/stores/budget.ts Budget store with isCostRecord type guard and per-filter WS deduplication — shares a single loading flag across three independent fetch functions, causing premature loading-state resets on concurrent calls.
web/src/stores/tasks.ts Task store with filter-awareness, optimistic local list updates, and per-event-type WS handling — clean and well-structured.
web/src/router/guards.ts Auth guard with redirect preservation and mustChangePassword TODO for PR 2 — correct for current scope; enforcement gap is explicitly documented.
web/src/composables/usePolling.ts setTimeout-based polling composable that avoids overlapping async calls and cleans up on unmount — well-implemented with minimum interval guard.
web/src/composables/useOptimisticUpdate.ts Optimistic update composable with in-flight guard, rollback on server error, and error isolation — correctly handles rollback failures without masking the original error.
web/nginx.conf nginx SPA + API/WS proxy with security headers — WS block correctly placed before /api/ prefix match, access_log off suppresses JWT query param logging, HSTS added.
web/src/api/types.ts Full TypeScript type definitions mirroring backend Pydantic models — discriminated union envelopes for ApiResponse and PaginatedResponse provide solid type narrowing.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant VueRouter
    participant AuthGuard
    participant PiniaAuthStore
    participant AxiosClient
    participant API

    Browser->>AxiosClient: API request (e.g. fetchTasks)
    AxiosClient->>API: GET /api/v1/tasks (Authorization: Bearer <token>)
    API-->>AxiosClient: 401 Unauthorized

    Note over AxiosClient: Response interceptor fires
    AxiosClient->>Browser: localStorage.removeItem('auth_token')
    AxiosClient->>VueRouter: router.push('/login') [dynamic import]

    VueRouter->>AuthGuard: authGuard(to=/login)
    AuthGuard->>PiniaAuthStore: auth.isAuthenticated?
    PiniaAuthStore-->>AuthGuard: true (token.value still set — localStorage not checked)
    AuthGuard->>VueRouter: next('/') ← redirects AWAY from login

    Note over Browser,PiniaAuthStore: User stuck at dashboard with stale Pinia state.<br/>Every API call omits Authorization (localStorage empty)<br/>→ 401 loop until hard refresh.

    Note over AxiosClient: Fix: lazy-import useAuthStore()<br/>and call clearAuth() alongside router.push()
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: web/src/api/client.ts
Line: 31-45

Comment:
**401 interceptor doesn't sync Pinia auth state — auth guard redirects user back to `/`**

The 401 handler clears `localStorage` and calls `router.push('/login')`, but the Pinia auth store's `token` ref is never cleared. When the navigation fires, `authGuard` evaluates `auth.isAuthenticated` which reads the still-truthy Pinia ref (`!!token.value === true`), sees the user as authenticated, and immediately calls `next('/')` — aborting the redirect to login.

The user ends up back at the dashboard in a broken state: the Pinia store thinks they are logged in, but every subsequent API request includes no `Authorization` header (because the request interceptor reads from `localStorage`, which is now empty), generating an endless stream of 401 errors until the page is hard-refreshed.

The `clearAuth()` path that actually fixes Pinia state is only triggered from `fetchUser()` (which only runs on initial auth or explicit call) — ordinary store mutations that hit 401 (e.g. a task fetch timing out) never get there.

The fix is to lazy-import the auth store inside the interceptor callback, the same pattern used for the router, to avoid circular dependency:

```ts
import('@/stores/auth').then(({ useAuthStore }) => {
  useAuthStore().clearAuth()
})
```

Or, alternatively, expose a lightweight event bus that `clearAuth` subscribes to, keeping the dependency graph clean.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: web/src/stores/company.ts
Line: 46-59

Comment:
**`fetchDepartments` can loop infinitely on an empty page with a non-zero total**

If the backend returns `{ data: [], total: N }` for any page (e.g. due to a race condition where items are deleted between pages, or a backend pagination bug), `allDepts.length` will never reach `result.total`, causing an unbounded loop that ties up the browser and the backend connection indefinitely.

Adding a guard on empty pages makes the loop unconditionally terminating:

```suggestion
        if (gen !== departmentsGen) return // Stale request — abort
        allDepts = [...allDepts, ...result.data]
        if (result.data.length === 0 || allDepts.length >= result.total) break
        offset += MAX_PAGE_SIZE
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: web/src/stores/messages.ts
Line: 65-76

Comment:
**Missing structural validation on WS `message.sent` payload**

Every other store that handles WebSocket events has an explicit runtime type-guard before treating the payload as a domain type (`isValidAgentPayload`, `isValidApprovalPayload`, `isValidTaskPayload`, `isCostRecord`). This handler skips that step and directly casts `event.payload` to `Message`, then only guards on `message.id`.

If the payload is structurally malformed — for example, `channel` is missing — and no `activeChannel` filter is set, the message is still appended to the list with `undefined` fields, which can cause downstream rendering failures.

Consider adding an `isValidMessagePayload` guard consistent with the other stores:

```ts
function isValidMessagePayload(p: Record<string, unknown>): boolean {
  return (
    typeof p.id === 'string' && p.id !== '' &&
    typeof p.channel === 'string' &&
    typeof p.sender === 'string' &&
    typeof p.content === 'string' &&
    typeof p.timestamp === 'string'
  )
}
```

Then guard the handler body with `if (!isValidMessagePayload(event.payload as Record<string, unknown>)) return`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: web/src/stores/budget.ts
Line: 53-90

Comment:
**Shared `loading` flag across three independent fetch operations**

`fetchConfig`, `fetchRecords`, and `fetchAgentSpending` all read/write the same `loading` ref. If two of them run concurrently:

1. Both set `loading = true`
2. The faster one finishes and sets `loading = false` in its `finally` block
3. The UI loading indicator disappears while the slower request is still in flight

This matters particularly for `fetchAgentSpending`, which is called per-agent and is likely to overlap with a background `fetchConfig` or `fetchRecords` poll.

Consider using separate loading flags (e.g. `configLoading`, `recordsLoading`, `spendingLoading`) similar to the pattern already used in `messages.ts` (`loading` / `channelsLoading`) and `company.ts` (`loading` / `departmentsLoading`).

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: be9cce7

Comment on lines +61 to +64
pendingSubscriptions = []
// Re-subscribe to all active subscriptions (covers both reconnect and first-connect)
for (const sub of activeSubscriptions) {
try {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JWT passed as query parameter in WebSocket URL

Passing the bearer token as a URL query parameter causes it to be recorded in server access logs, nginx access logs, browser history, and any intermediate proxy logs in plaintext. The existing TODO comment acknowledges this, but the risk is worth surfacing clearly:

  • Every nginx proxy_pass will log the full request URL including the query string by default.
  • Browser history and tab-restore features may persist the URL.
  • Shared/cached CDN or proxy infrastructure may log or cache the full URL.

Until the one-time ticket endpoint from #343 is implemented, consider suppressing query-string logging in nginx.conf for the /api/v1/ws location (e.g. access_log off; or a custom log format that omits query strings) to limit the exposure window.

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/stores/websocket.ts
Line: 61-64

Comment:
**JWT passed as query parameter in WebSocket URL**

Passing the bearer token as a URL query parameter causes it to be recorded in server access logs, nginx access logs, browser history, and any intermediate proxy logs in plaintext. The existing TODO comment acknowledges this, but the risk is worth surfacing clearly:

- Every nginx `proxy_pass` will log the full request URL including the query string by default.
- Browser history and tab-restore features may persist the URL.
- Shared/cached CDN or proxy infrastructure may log or cache the full URL.

Until the one-time ticket endpoint from `#343` is implemented, consider suppressing query-string logging in `nginx.conf` for the `/api/v1/ws` location (e.g. `access_log off;` or a custom log format that omits query strings) to limit the exposure window.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Establishes the foundational Vue 3 + PrimeVue + Tailwind CSS web dashboard infrastructure (API client/types, stores, router/auth, shared UI components, build/test tooling, container/CI wiring) as the base for the page/feature PR that follows.

Changes:

  • Added Vite/Vitest/TS/ESLint configs plus core dashboard app bootstrap (router, Pinia, PrimeVue, global styles).
  • Introduced typed API client + endpoint modules and Pinia stores (including WebSocket + real-time event handling).
  • Added unit tests, nginx/Docker/CI updates, and documentation updates reflecting the dashboard’s new status.

Reviewed changes

Copilot reviewed 84 out of 88 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
web/vitest.config.ts Vitest config (Vue plugin, alias, coverage)
web/vite.config.ts Vite config (Vue + Tailwind, proxy, alias)
web/tsconfig.node.json TS config for Node-side tooling
web/tsconfig.json TS config for app + Vitest globals
web/style.css Removed placeholder static CSS
web/src/views/SetupPage.vue Initial admin setup view
web/src/views/PlaceholderHome.vue Temporary home placeholder
web/src/views/LoginPage.vue Login view + lockout UX
web/src/utils/format.ts Formatting helpers
web/src/utils/errors.ts Error classification + safe messages
web/src/utils/constants.ts App constants + navigation + status maps
web/src/styles/theme.ts Theme tokens + badge class maps
web/src/styles/global.css Tailwind v4 import + global CSS
web/src/stores/websocket.ts WebSocket store (reconnect/subscriptions/dispatch)
web/src/stores/tasks.ts Tasks store + WS event integration
web/src/stores/providers.ts Providers store + secret stripping
web/src/stores/messages.ts Messages store + channel filtering
web/src/stores/company.ts Company config/departments store
web/src/stores/budget.ts Budget store + WS record ingestion
web/src/stores/auth.ts Auth store (token lifecycle, profile fetch)
web/src/stores/approvals.ts Approvals store + WS updates
web/src/stores/analytics.ts Analytics store
web/src/stores/agents.ts Agents store + WS updates
web/src/router/index.ts Router setup + routes + guard hook
web/src/router/guards.ts Auth navigation guard
web/src/primevue-preset.ts PrimeVue Aura preset options
web/src/main.ts App bootstrap + global error handlers
web/src/composables/usePolling.ts Polling composable (non-overlapping)
web/src/composables/useOptimisticUpdate.ts Optimistic update composable
web/src/composables/useAuth.ts Auth convenience composable
web/src/components/layout/Topbar.vue Topbar + user menu + disconnect/logout
web/src/components/layout/Sidebar.vue Sidebar navigation + collapse
web/src/components/layout/ConnectionStatus.vue API/WS status indicator
web/src/components/layout/AppShell.vue Shell layout wiring
web/src/components/common/StatusBadge.vue Status/priority/risk badge component
web/src/components/common/PageHeader.vue Page header component
web/src/components/common/LoadingSkeleton.vue Skeleton loader wrapper
web/src/components/common/ErrorBoundary.vue Error boundary component
web/src/components/common/EmptyState.vue Empty-state component
web/src/api/types.ts Frontend DTO/type mirror definitions
web/src/api/endpoints/tasks.ts Tasks API endpoints
web/src/api/endpoints/providers.ts Providers API endpoints
web/src/api/endpoints/messages.ts Messages API endpoints
web/src/api/endpoints/health.ts Health API endpoint
web/src/api/endpoints/company.ts Company/departments endpoints
web/src/api/endpoints/budget.ts Budget endpoints
web/src/api/endpoints/auth.ts Auth endpoints
web/src/api/endpoints/approvals.ts Approvals endpoints
web/src/api/endpoints/analytics.ts Analytics endpoint
web/src/api/endpoints/agents.ts Agents/autonomy endpoints
web/src/api/client.ts Axios client + interceptors + unwrap helpers
web/src/tests/utils/format.test.ts Tests for formatting utilities
web/src/tests/utils/errors.test.ts Tests for error utilities
web/src/tests/utils/constants.test.ts Tests for constants/maps
web/src/tests/stores/websocket.test.ts WebSocket store tests
web/src/tests/stores/tasks.test.ts Tasks store tests
web/src/tests/stores/messages.test.ts Messages store tests
web/src/tests/stores/budget.test.ts Budget store tests
web/src/tests/stores/auth.test.ts Auth store tests
web/src/tests/stores/approvals.test.ts Approvals store tests
web/src/tests/stores/agents.test.ts Agents store tests
web/src/tests/router/guards.test.ts Router guard tests
web/src/tests/composables/usePolling.test.ts Polling composable tests
web/src/tests/composables/useOptimisticUpdate.test.ts Optimistic update tests
web/src/tests/components/StatusBadge.test.ts StatusBadge component tests
web/src/tests/components/PageHeader.test.ts PageHeader component tests
web/src/tests/components/EmptyState.test.ts EmptyState component tests
web/src/tests/api/client.test.ts API client unwrap/interceptor tests
web/src/App.vue Root app component (Toast/Confirm/RouterView)
web/public/favicon.svg Dashboard favicon
web/package.json Dashboard dependencies + scripts
web/nginx.conf SPA routing + API/WS proxy + CSP updates
web/index.html App entry HTML for Vite SPA
web/eslint.config.js ESLint flat config (Vue + security)
web/env.d.ts Vite/Vue TS declarations
web/app.js Removed legacy placeholder script
web/.env.example Example env var for API base URL
docs/user_guide.md Docs: reflect dashboard availability
docs/roadmap/index.md Roadmap: dashboard marked “In Progress”
docs/getting_started.md Getting started: web dev instructions
docs/design/operations.md Design docs: web UI status update
docker/web/Dockerfile Multi-stage dashboard build + nginx runtime
README.md Status section updated for dashboard progress
CLAUDE.md Repo layout + web dev commands documented
.gitignore Ignore dashboard build/coverage artifacts
.github/workflows/ci.yml CI: add dashboard lint/type-check/test/build/audit
.dockerignore Ignore dashboard artifacts in Docker context

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +8 to +11
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
Comment on lines +7 to +11
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
Comment on lines +104 to +108
/** Discriminated paginated response envelope. */
export type PaginatedResponse<T> =
| { data: T[]; error: null; success: true; pagination: PaginationMeta }
| { data: null; error: string; success: false; pagination: null }

Comment on lines +13 to +17
const result: Record<string, ProviderConfig> = {}
for (const [key, provider] of Object.entries(raw)) {
result[key] = stripSecrets(provider)
}
return result
Comment on lines +213 to +216
function dispatchEvent(event: WsEvent) {
channelHandlers.get(event.channel)?.forEach((h) => { h(event) })
channelHandlers.get('*')?.forEach((h) => { h(event) })
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 41

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLAUDE.md`:
- Line 125: Narrow the scope of the inline examples on the "views/" line by
removing the deferred full-feature page names (Dashboard, TaskBoard,
AgentProfiles, BudgetPanel) and replace them with only the view components
actually added in this PR or a neutral phrase like "Page-level components
(current PR views only)"; edit the comment on the line that contains "views/" to
either list the specific in-repo view names introduced by this PR or remove the
example list altogether so it no longer claims pages that are deferred to PR 2.

In `@web/nginx.conf`:
- Around line 33-45: The X-Forwarded-Proto header is being overwritten with
$scheme (which is always "http" here) — change this to preserve an incoming
header with an http-scope fallback by introducing a map (e.g. map
$http_x_forwarded_proto $x_forwarded_proto { default $http_x_forwarded_proto; ''
$scheme; }) and then set proxy_set_header X-Forwarded-Proto $x_forwarded_proto
in both the location /api/v1/ws and location /api/ blocks so the original client
protocol is preserved when present and $scheme is used only as a fallback.

In `@web/package.json`:
- Around line 7-13: The package.json scripts ("dev", "build", "preview") call
the Vite CLI but Vite is not declared in devDependencies; add an explicit
devDependency entry for "vite" (e.g., by running the package manager command to
install it as a devDependency or by adding "vite": "<compatible-version>" under
devDependencies) so the CLI is guaranteed to be available; ensure the version
you choose is compatible with existing dev tools like "vitest" and
"@vitejs/plugin-vue" to avoid resolution conflicts.

In `@web/src/__tests__/api/client.test.ts`:
- Around line 28-33: The test currently guards the interceptor call with "if
(interceptor)" which can silently skip assertions; instead assert the
interceptor exists and is a function before invoking it: add an explicit
expect(typeof interceptor).toBe('function') (or similar) targeting the
interceptor variable extracted from apiClient.interceptors.request.handlers so
the test fails if the request interceptor is not registered; apply this change
to both test cases that extract "handlers" / "interceptor" (the blocks around
apiClient.interceptors.request and the subsequent config invocation).

In `@web/src/__tests__/components/EmptyState.test.ts`:
- Around line 49-55: Rename the existing test in EmptyState.test.ts to "does not
render action container when slot is not provided" to match its behavior, and
add a new test that mounts the EmptyState component with an explicitly provided
but empty action slot (e.g., pass slots: { action: '' } or a slot function that
returns null) to assert the '.mt-4' action container is not rendered; reference
the EmptyState component and the test file's existing
expect(wrapper.findAll('.mt-4')).toHaveLength(0) assertion to validate both
scenarios.

In `@web/src/__tests__/components/PageHeader.test.ts`:
- Around line 5-27: Add a test to PageHeader.test.ts that mounts PageHeader with
an actions slot and asserts the slot content is rendered; specifically, in the
describe('PageHeader') block add an it('renders actions slot', ...) that mounts
PageHeader (using mount(PageHeader, { props: {...}, slots: { actions:
'<button>Action</button>' } })) and then checks wrapper.find('button') or
wrapper.text() contains 'Action' to ensure the actions slot contract in
PageHeader is preserved.

In `@web/src/__tests__/components/StatusBadge.test.ts`:
- Around line 31-52: Add an explicit assertion that the element exists before
checking its classes to make failures clearer: in the test cases that call
wrapper.find('.p-tag') (the variable named tag in the StatusBadge tests), add an
existence assertion (e.g., expect(tag.exists())) immediately after finding the
element and before the expect(tag.classes()) assertions so structural rendering
changes fail with a clear message rather than class mismatches.

In `@web/src/__tests__/composables/useOptimisticUpdate.test.ts`:
- Around line 64-76: The test creates a console.error spy (consoleSpy) but only
calls consoleSpy.mockRestore() on the happy path, risking leaked mocks if
assertions fail; update the test around execute(...) / assertions to ensure
consoleSpy.mockRestore() always runs by moving restoration into a finally block
or using an afterEach that restores console.error, referencing the existing
consoleSpy and the execute / error.value assertions so the spy is always removed
even on failures.

In `@web/src/__tests__/stores/agents.test.ts`:
- Around line 61-100: Tests for useAgentStore.handleWsEvent are missing edge
cases for agent.hired: deduplication and malformed payloads; add two tests
referencing useAgentStore and handleWsEvent that: 1) send a second agent.hired
WS event with the same unique identifier as mockAgent (e.g., name or id in
mockAgent) and assert agents length and total do not increase (still 1), and 2)
send an agent.hired event with a malformed payload (e.g., missing name/id or
required fields) and assert the store does not add the agent or change total,
ensuring handleWsEvent validates payload before inserting.

In `@web/src/__tests__/stores/approvals.test.ts`:
- Around line 212-224: The test for preventing duplicate approvals only asserts
approvals array length but misses verifying that the store.total counter also
doesn't increment; update the test that uses useApprovalStore and calls
store.handleWsEvent(event) to also assert store.total remains 1 (and optionally
unchanged from its pre-event value) after the event to catch regressions in the
total counter handling for duplicate approval.submitted events involving
mockApproval.

In `@web/src/__tests__/stores/budget.test.ts`:
- Around line 45-46: The test fixtures are untyped and use incorrect field names
so they can drift from the real contract; change the mocked responses to be
typed against the shared BudgetConfig and spending interfaces (import
BudgetConfig and the spending type from web/src/api/types.ts) and construct mock
objects that match those types — e.g., replace daily_limit/total_budget with the
actual BudgetConfig fields and replace total_cost with total_cost_usd in the
spending fixture used by mockGetBudgetConfig and the spending mock (e.g.,
mockGetBudgetSpending or whatever mock is used on lines ~93-99) so the mocks
fail at compile time if the API types change.

In `@web/src/api/endpoints/agents.ts`:
- Around line 23-24: The post call in agents.ts doesn't supply the generic type
so unwrap loses the ApiResponse envelope and compile-time guarantees for
AutonomyLevelResponse; update the apiClient.post invocation used in the function
that calls `/agents/${encodeURIComponent(agentId)}/autonomy` to use the generic
ApiResponse<AutonomyLevelResponse> (e.g.,
apiClient.post<ApiResponse<AutonomyLevelResponse>>(...)) so the response retains
the envelope type expected by unwrap and preserves type safety across agents.ts
consistent with auth.ts.

In `@web/src/api/endpoints/providers.ts`:
- Around line 13-16: The provider map construction writes untrusted keys into a
plain object and can be poisoned (e.g., "__proto__"); change the map to a safe
dictionary and skip dangerous keys: create result with Object.create(null)
instead of {}, and in the loop that iterates over Object.entries(raw) (the block
that calls stripSecrets(provider) and assigns to result[key]) skip keys like
"__proto__", "constructor", and "prototype" (or otherwise validate keys) before
assigning. This keeps using stripSecrets(provider) and the same variable names
(result, raw, key, provider) but prevents prototype pollution.

In `@web/src/components/common/EmptyState.vue`:
- Around line 18-20: The proposed change wrongly invokes the slot function in
setup (e.g., calling slots.action?.()), which violates Vue 3 render-phase rules
and can trigger warnings; revert to the idiomatic pattern used now (keep the
template check v-if="$slots.action" in EmptyState.vue and do not call
$slots.action or slots.action() outside the render function), and if you truly
need to detect empty slot content perform that invocation only during render (or
mirror the existing approach used in PageHeader.vue) rather than computing it in
setup.

In `@web/src/components/common/ErrorBoundary.vue`:
- Around line 1-11: The component exported as ErrorBoundary does not implement
Vue's error capture lifecycle (onErrorCaptured) and only displays an error
passed via props, so either rename the component/file to a more accurate name
like ErrorDisplay or ErrorState and update all imports/usages, or keep the name
but add a top-of-file doc comment clarifying it expects an external error prop
and does not capture child errors; update references to the component
accordingly and consider implementing onErrorCaptured inside the <script setup>
(using onErrorCaptured to set local state and emit retry) if you intend true
boundary behavior (see defineProps and defineEmits for where to wire this).

In `@web/src/components/layout/ConnectionStatus.vue`:
- Around line 27-56: The status dots in ConnectionStatus.vue are purely visual
and need accessible labels; update the API and WS indicator elements (the span
with classes using healthError/health?.status and the span using
wsStore.connected) to include appropriate ARIA attributes and hidden descriptive
text: add role="status" or aria-live="polite" on a wrapper, provide aria-label
or aria-describedby that reflects the current state (e.g., "API:
ok/degraded/error" derived from health and healthError, and "WebSocket:
connected/disconnected" from wsStore.connected), and include visually-hidden
text nodes for screen readers so the indicators convey the same semantic state
as the color dots.

In `@web/src/components/layout/Sidebar.vue`:
- Around line 2-3: The component is hardcoding the app name "SynthOrg" instead
of using the shared constant; import APP_NAME from '@/utils/constants' alongside
NAV_ITEMS in Sidebar.vue and replace all inline "SynthOrg" string occurrences
(including the other instances around where NAV_ITEMS is used) with the APP_NAME
symbol so the brand comes from the single source-of-truth constant.

In `@web/src/components/layout/Topbar.vue`:
- Around line 47-67: Add accessible names for the icon-only sidebar toggle and
the user-menu button: update the <Button> for the sidebar toggle (the one with
icon="pi pi-bars" and `@click`="$emit('toggleSidebar')") to include an aria-label
(e.g., aria-label="Toggle sidebar" or a localized equivalent), and update the
user button (the <Button> with :label="auth.user?.username ?? ''", icon="pi
pi-user" and `@click`="toggleUserMenu") to bind a fallback accessible name when
auth.user is null (e.g., :aria-label="auth.user?.username ?? 'Open user menu'"),
ensuring the Menu ref="userMenu" and toggleUserMenu logic continue to work
unchanged.

In `@web/src/composables/useAuth.ts`:
- Around line 14-16: The permission check widens WRITE_ROLES to string[] which
loses the role union and disables compiler checks; remove the cast in canWrite
and call includes against the properly typed constant (e.g.,
WRITE_ROLES.includes(role)), and ensure WRITE_ROLES is declared with the correct
readonly union type (for example as readonly Role[] or `const WRITE_ROLES = [
... ] as const` so its element type matches userRole) so the lookup preserves
the role union and lets TypeScript catch typos/drift; update either the
WRITE_ROLES declaration or its exported type rather than casting inside useAuth
(references: canWrite, userRole, WRITE_ROLES).

In `@web/src/composables/useOptimisticUpdate.ts`:
- Around line 22-33: The current execute() calls applyOptimistic() which both
builds the rollback and performs mutations, so if it throws after mutating we
lose rollback; refactor so rollback is captured before any mutation: change the
optimistic contract into two steps (e.g., prepareOptimistic/commitOptimistic or
have applyOptimistic return a pure rollback-builder that you call before
applying mutations), then in execute() first obtain and store the rollback
callback synchronously, then perform the actual optimistic mutation, keeping
references to rollback and ensuring the catch block can always call it; update
call sites to the new two-step API and adjust types for
applyOptimistic/prepareOptimistic accordingly.

In `@web/src/router/index.ts`:
- Line 6: The router currently calls createWebHistory() with a hardcoded root;
update the router to pass Vite's base URL so it respects subpath deployments by
calling createWebHistory with the Vite base (import.meta.env.BASE_URL). Locate
the createWebHistory invocation in web/src/router/index.ts and replace the
no-arg call so the router uses import.meta.env.BASE_URL (or the app's equivalent
env constant) as the base argument to createWebHistory, ensuring routes and
refreshes work when Vite's base is set.

In `@web/src/stores/agents.ts`:
- Around line 28-34: The fetchAgent function can leave a stale error visible
after a subsequent successful call; clear the reactive error state before
attempting the request and also ensure it's cleared on success. Specifically, in
fetchAgent (which calls agentsApi.getAgent and sets error.value =
getErrorMessage(err) on catch), set error.value = '' at the start of the
function and clear it again after a successful await to guarantee no old error
message remains.
- Around line 38-49: The handler for event.event_type 'agent.hired' currently
casts a partial payload to AgentConfig and pushes it into agents.value, which
can insert incomplete objects; instead add a runtime validation/type guard
(e.g., isValidAgentPayload) that checks all required top-level and nested fields
of AgentConfig (id, name, role, department, and any nested required properties),
and only when it returns true construct a concrete AgentConfig object (populate
required fields explicitly, fill optional fields with safe defaults if needed)
and push that into agents.value and increment total.value; remove the direct
cast (payload as AgentConfig) and return early for invalid payloads to avoid
storing malformed agents.

In `@web/src/stores/approvals.ts`:
- Around line 15-21: fetchApprovals currently doesn't record the active query,
so subsequent local mutations (approve, reject) and handleWsEvent mutate
approvals/total as if unfiltered and can leave non-matching rows; add a
store-scoped activeQuery (e.g., activeFilters/offset/limit) and set it inside
fetchApprovals before mutating approvals, then update approve, reject, and
handleWsEvent to either refetch via approvalsApi.listApprovals(activeQuery) or
reapply the stored filter and correctly adjust total so mutations/events only
affect the currently active view (reference fetchApprovals, approve, reject,
handleWsEvent, approvals, total, approvalsApi.listApprovals).
- Around line 54-65: The code casts a partial WebSocket payload to ApprovalItem
and pushes it into approvals.value on 'approval.submitted', which can produce
malformed records because only id, action_type, and title are checked; instead,
implement a runtime guard or hydrate before inserting: validate that payload
contains all required ApprovalItem fields (description, requested_by,
risk_level, status, metadata, created_at, etc.) or fetch the full approval by id
and only then push a complete ApprovalItem into approvals.value and increment
total.value; update the logic around event.event_type === 'approval.submitted',
payload, and approvals.value so you never insert payload as ApprovalItem without
full validation/hydration.

In `@web/src/stores/auth.ts`:
- Around line 126-127: The console.error call in the auth flow is logging the
full error object (the line with "Failed to fetch user profile:"), which may
include sensitive metadata; update the logging inside the function that fetches
the user profile (e.g., fetchUserProfile / the handler around that
console.error) to log a sanitized error instead—construct a minimal object with
only safe fields like { message: err?.message, code: err?.code } or call a small
sanitizeError(err) helper that strips stack/headers/body, use that in place of
the raw err in the log, and keep rethrowing the original error (throw err)
unchanged so behavior is preserved.

In `@web/src/stores/budget.ts`:
- Around line 35-41: The fetchRecords call currently doesn't preserve its params
so the budget.record_added WS handler prepends/unconditionally increments
records; store the last used params (e.g., lastFetchParams) when
fetchRecords(params) runs, and update the budget.record_added handler to (1)
ignore events whose agent_id or task_id don't match lastFetchParams (if
present), (2) when prepending a new record update records.value and
totalRecords.value but enforce the lastFetchParams.limit by trimming
records.value to that limit, and (3) when incrementing counts use
lastFetchParams to decide whether to apply the change; update references to
records.value and totalRecords.value and the budget.record_added handler
accordingly.
- Around line 10-13: isCostRecord currently only checks agent_id and cost_usd
which allows partial objects to be treated as full CostRecord and then stored
into records; update isCostRecord to validate every required CostRecord property
(e.g. agent_id: string, cost_usd: number, task_id: string, provider: string,
call_category: string, and timestamp as a number or valid ISO string) so it
reliably narrows unknown to CostRecord before insertion into records; adjust the
predicate implementation (isCostRecord) to cast payload to Record<string,
unknown> and explicitly check each field's existence and type/format, returning
false if any are missing or invalid.

In `@web/src/stores/company.ts`:
- Around line 44-47: fetchDepartments currently assigns only the first page from
companyApi.listDepartments, losing items when total > limit; change it to
repeatedly request pages (using MAX_PAGE_SIZE) until you've retrieved
result.meta.total items, concatenating each result.data into departments.value.
In each loop iteration call companyApi.listDepartments with the appropriate
pagination param (offset or page number / cursor) to fetch the next page, check
gen === departmentsGen before merging to avoid races, and break early if gen
changes; ensure the final departments.value contains all fetched pages.

In `@web/src/stores/messages.ts`:
- Around line 31-43: The fetchMessages function can be overwritten by
out-of-order responses; add a per-call request token (e.g. incrementing
currentRequestId stored outside the function) or use an AbortController to
cancel previous calls before calling messagesApi.listMessages, capture the
token/abort signal locally, and only assign messages.value, total.value, and
error.value (and clear loading.value) if the token matches the latestRequestId
(or the request was not aborted); ensure the token is set before awaiting
messagesApi.listMessages and checked in both the try and finally blocks so stale
responses do not commit state.

In `@web/src/stores/tasks.ts`:
- Around line 112-130: The handleWsEvent function currently casts event.payload
to Partial<Task> and may merge malformed payloads into tasks.value; add a
runtime guard that validates required Task fields (at minimum id plus other
non-nullable fields your UI expects) and types before appending or merging: in
the 'task.created' case validate payload is a complete Task object (or bail/log)
before pushing to tasks.value and incrementing total, and in the
'task.updated'/'task.status_changed'/'task.assigned' cases validate payload.id
and any fields you will merge (e.g., status, assignee) to avoid overwriting with
invalid values; use the existing WsEvent, Task, handleWsEvent, hasActiveFilters
and tasks.value symbols to locate where to perform these checks and return early
or log and ignore the event when validation fails.
- Around line 57-65: When creating a task in createTask, don’t unconditionally
append to tasks.value and bump total.value; first check currentFilters and
pagination context to ensure the new task would actually appear in the current
view. Implement a match check (e.g., reuse or add a helper like
matchesFilters(task, currentFilters) or reuse existing filter logic) and only
push into tasks.value and increment total.value when the created task satisfies
currentFilters (and fits current page criteria, e.g., on first page when
paginated); otherwise skip mutating tasks.value/total.value so the local
collection remains consistent with filtered/paginated views.
- Around line 42-47: In fetchTasks(), currentFilters.value is only replaced when
a filters argument is provided, so calling fetchTasks() without args reuses
previous filters; update fetchTasks (the async function fetchTasks(filters?:
TaskFilters)) to set currentFilters.value = { } (or an empty TaskFilters object)
when filters is undefined so callers that omit filters clear previously applied
filters and allow hasActiveFilters() to reflect no active filters.

In `@web/src/stores/websocket.ts`:
- Around line 50-53: The code currently embeds the JWT into the WebSocket URL
(getWsUrl() and token used to construct new WebSocket(url)) which leaks secrets;
change the flow to not send token as a query param—either implement the TODO
ticket handshake (request a one-time WS ticket from the server via XHR/fetch and
use that short-lived ticket instead of the JWT) or switch to a non-URL auth
mechanism (e.g., pass a one-time ticket via the Sec-WebSocket-Protocol or
upgrade over an authenticated HTTP handshake), update the socket creation to use
the ticket or protocol instead of `?token=...`, and remove or sanitize the raw
error logging for the WebSocket error handler (the code that logs the error
event) to avoid echoing secrets. Ensure references to getWsUrl(), token, new
WebSocket(...), and the socket error logging are updated accordingly.
- Around line 178-190: The unsubscribe() implementation currently checks
whole-subscription equality using channels.every(), which prevents removing
subscriptions that partially overlap (e.g., ['tasks','agents'] vs ['tasks']),
causing removed channels to be re-subscribed after reconnect; update
unsubscribe() to iterate activeSubscriptions and pendingSubscriptions and for
each subscription remove only the channels present in the unsubscribe set
(modify the subscription's channels array to filter out channelSet members) and
if a subscription's channels becomes empty, remove that subscription entry
entirely; ensure you use the existing variables activeSubscriptions,
pendingSubscriptions, and channelSet to perform in-place updates so reconnect
logic respects partial unsubscriptions.
- Around line 7-8: The regex in sanitizeLogValue is triggering the
noControlCharactersInRegex lint rule; instead of using a control-character range
in the regex, change sanitizeLogValue to filter characters by their code points
(e.g., iterate over the string or use Array.from and keep only chars with
charCodeAt >= 0x20 and != 0x7f) and then truncate to max, or alternatively add a
localized lint suppression comment above sanitizeLogValue if you intentionally
want the regex—prefer the refactor to a charCode-based filter to avoid the lint
rule and preserve behavior.

In `@web/src/utils/constants.ts`:
- Around line 55-66: NAV_ITEMS currently exposes links for routes the router
doesn't register yet (only /login, /setup and / exist), causing clicks to be
caught by the catch-all redirect; either remove or comment out the unreleased
entries in NAV_ITEMS (Org Chart, Tasks, Messages, Approvals, Agents, Budget,
Meetings, Artifacts, Settings) until the router supports them, or alternatively
add temporary placeholder routes for each path in your router setup so those
paths resolve instead of redirecting—update NAV_ITEMS or the router consistently
so navigation items only point to registered routes.

In `@web/src/utils/errors.ts`:
- Around line 55-61: The current check allows any plain Error through (error
instanceof Error) which lets backend messages leak; change the logic to only
pass through messages when the error is an explicit frontend-safe type (e.g.
FrontendError or SafeFrontendError) and reject plain Error instances. Update the
condition around error instanceof Error to instead check for the safe class
(e.g. error instanceof FrontendError) and keep the existing msg length/format
guards; for all other Error instances return the generic sanitized message. If
FrontendError doesn't exist yet, add a simple exported class (or type guard)
used by client code when throwing safe errors, and use that class name in the
check within the function.

In `@web/src/utils/format.ts`:
- Around line 60-68: formatUptime currently allows NaN and negative inputs which
produce invalid output like "NaNm" or "-1m"; update the formatUptime function to
validate and clamp its input by first checking Number.isFinite(seconds) and
seconds >= 0 (or coerce any invalid value to 0) before computing
days/hours/mins, then proceed with the existing breakdown and return; this
ensures the output is never negative or contains "NaN" and will produce a safe
fallback like "0m" for bad inputs.

In `@web/src/views/LoginPage.vue`:
- Around line 21-58: Extract the lockout logic into a shared composable (e.g.,
useLockout) that centralizes now/clockTimer, locked (computed from lockedUntil
and now), checkAndClearLockout, attempts and lockedUntil and exposes functions:
isLocked(), recordCredentialFailure(), and clearFailures(); update handleLogin
to only call recordCredentialFailure() when auth.login fails with a credential
error (not on network/5xx — detect via getErrorMessage or error type), and
derive the visible error text from isLocked()/lockedUntil+now rather than
setting error.value directly on lock creation; replace duplicated blocks in
LoginPage.vue and SetupPage.vue to call the composable APIs (use isLocked() to
short-circuit and to render the lockout message, and recordCredentialFailure()
on confirmed bad-credentials).

In `@web/src/views/PlaceholderHome.vue`:
- Around line 2-3: The loading placeholder div in PlaceholderHome.vue currently
renders passive text; add ARIA live-region semantics by updating the same div
(the element with class "flex items-center justify-center h-full
text-slate-400") to include live-region attributes such as role="status" and
aria-live="polite" (optionally aria-busy="true" while loading) so screen readers
announce the "Dashboard loading…" message during route loads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 01b70be5-e36d-4ef0-8c09-ed77f18d0ea7

📥 Commits

Reviewing files that changed from the base of the PR and between 2f10d49 and bee3786.

⛔ Files ignored due to path filters (2)
  • web/package-lock.json is excluded by !**/package-lock.json
  • web/public/favicon.svg is excluded by !**/*.svg
📒 Files selected for processing (86)
  • .dockerignore
  • .github/workflows/ci.yml
  • .gitignore
  • CLAUDE.md
  • README.md
  • docker/web/Dockerfile
  • docs/design/operations.md
  • docs/getting_started.md
  • docs/roadmap/index.md
  • docs/user_guide.md
  • web/.env.example
  • web/app.js
  • web/env.d.ts
  • web/eslint.config.js
  • web/index.html
  • web/nginx.conf
  • web/package.json
  • web/src/App.vue
  • web/src/__tests__/api/client.test.ts
  • web/src/__tests__/components/EmptyState.test.ts
  • web/src/__tests__/components/PageHeader.test.ts
  • web/src/__tests__/components/StatusBadge.test.ts
  • web/src/__tests__/composables/useOptimisticUpdate.test.ts
  • web/src/__tests__/composables/usePolling.test.ts
  • web/src/__tests__/router/guards.test.ts
  • web/src/__tests__/stores/agents.test.ts
  • web/src/__tests__/stores/approvals.test.ts
  • web/src/__tests__/stores/auth.test.ts
  • web/src/__tests__/stores/budget.test.ts
  • web/src/__tests__/stores/messages.test.ts
  • web/src/__tests__/stores/tasks.test.ts
  • web/src/__tests__/stores/websocket.test.ts
  • web/src/__tests__/utils/constants.test.ts
  • web/src/__tests__/utils/errors.test.ts
  • web/src/__tests__/utils/format.test.ts
  • web/src/api/client.ts
  • web/src/api/endpoints/agents.ts
  • web/src/api/endpoints/analytics.ts
  • web/src/api/endpoints/approvals.ts
  • web/src/api/endpoints/auth.ts
  • web/src/api/endpoints/budget.ts
  • web/src/api/endpoints/company.ts
  • web/src/api/endpoints/health.ts
  • web/src/api/endpoints/messages.ts
  • web/src/api/endpoints/providers.ts
  • web/src/api/endpoints/tasks.ts
  • web/src/api/types.ts
  • web/src/components/common/EmptyState.vue
  • web/src/components/common/ErrorBoundary.vue
  • web/src/components/common/LoadingSkeleton.vue
  • web/src/components/common/PageHeader.vue
  • web/src/components/common/StatusBadge.vue
  • web/src/components/layout/AppShell.vue
  • web/src/components/layout/ConnectionStatus.vue
  • web/src/components/layout/Sidebar.vue
  • web/src/components/layout/Topbar.vue
  • web/src/composables/useAuth.ts
  • web/src/composables/useOptimisticUpdate.ts
  • web/src/composables/usePolling.ts
  • web/src/main.ts
  • web/src/primevue-preset.ts
  • web/src/router/guards.ts
  • web/src/router/index.ts
  • web/src/stores/agents.ts
  • web/src/stores/analytics.ts
  • web/src/stores/approvals.ts
  • web/src/stores/auth.ts
  • web/src/stores/budget.ts
  • web/src/stores/company.ts
  • web/src/stores/messages.ts
  • web/src/stores/providers.ts
  • web/src/stores/tasks.ts
  • web/src/stores/websocket.ts
  • web/src/styles/global.css
  • web/src/styles/theme.ts
  • web/src/utils/constants.ts
  • web/src/utils/errors.ts
  • web/src/utils/format.ts
  • web/src/views/LoginPage.vue
  • web/src/views/PlaceholderHome.vue
  • web/src/views/SetupPage.vue
  • web/style.css
  • web/tsconfig.json
  • web/tsconfig.node.json
  • web/vite.config.ts
  • web/vitest.config.ts
💤 Files with no reviewable changes (2)
  • web/app.js
  • web/style.css
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
  • GitHub Check: Test (Python 3.14)
🧰 Additional context used
📓 Path-based instructions (3)
docs/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Docs source: docs/ (Markdown, built with Zensical). Design spec: docs/design/ (7 pages: index, agents, organization, communication, engine, memory, operations).

Files:

  • docs/user_guide.md
  • docs/roadmap/index.md
  • docs/design/operations.md
  • docs/getting_started.md
.github/workflows/*.yml

📄 CodeRabbit inference engine (CLAUDE.md)

CI: Jobs (lint + type-check + test in parallel) → ci-pass gate. Pages workflow exports OpenAPI schema, builds Astro landing + Zensical docs, merges, deploys to GitHub Pages on push to main.

Files:

  • .github/workflows/ci.yml
docker/**

📄 CodeRabbit inference engine (CLAUDE.md)

Docker: Backend 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web: nginxinc/nginx-unprivileged, SPA routing, API/WebSocket proxy to backend. All Docker files in docker/ directory.

Files:

  • docker/web/Dockerfile
🧠 Learnings (7)
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to docs/**/*.md : Docs source: `docs/` (Markdown, built with Zensical). Design spec: `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations).

Applied to files:

  • .gitignore
  • README.md
  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to docker/** : Docker: Backend 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web: `nginxinc/nginx-unprivileged`, SPA routing, API/WebSocket proxy to backend. All Docker files in `docker/` directory.

Applied to files:

  • .gitignore
  • docs/roadmap/index.md
  • .dockerignore
  • docs/getting_started.md
  • docker/web/Dockerfile
  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to .github/workflows/*.yml : CI: Jobs (lint + type-check + test in parallel) → ci-pass gate. Pages workflow exports OpenAPI schema, builds Astro landing + Zensical docs, merges, deploys to GitHub Pages on push to main.

Applied to files:

  • .github/workflows/ci.yml
  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to .github/workflows/docker.yml : Docker CI: `.github/workflows/docker.yml` builds backend + web images, pushes to GHCR, signs with cosign. Scans: Trivy (CRITICAL = hard fail, HIGH = warn-only) + Grype (critical cutoff). CVE triage via `.github/.trivyignore.yaml` and `.github/.grype.yaml`. Images only pushed after scans pass.

Applied to files:

  • .github/workflows/ci.yml
  • docker/web/Dockerfile
  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to pyproject.toml : Dependencies: all versions pinned with `==` in `pyproject.toml`. Groups: `test` and `dev` (includes test). Install via `uv sync`.

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to src/**/*.py : Linting: `uv run ruff check src/ tests/` and format with `uv run ruff format src/ tests/`

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to src/**/*.py : Type-checking: `uv run mypy src/ tests/` (strict mode)

Applied to files:

  • CLAUDE.md
🧬 Code graph analysis (36)
web/src/router/guards.ts (1)
web/src/stores/auth.ts (1)
  • useAuthStore (8-165)
web/src/api/endpoints/company.ts (2)
web/src/api/types.ts (5)
  • CompanyConfig (392-396)
  • ApiResponse (94-96)
  • PaginationParams (552-555)
  • Department (381-385)
  • PaginatedResponse (105-107)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrap (54-60)
  • unwrapPaginated (66-82)
web/src/__tests__/utils/format.test.ts (1)
web/src/utils/format.ts (6)
  • formatDate (6-17)
  • formatRelativeTime (22-36)
  • formatCurrency (41-48)
  • formatNumber (53-55)
  • formatUptime (60-69)
  • formatLabel (74-79)
web/src/api/endpoints/messages.ts (2)
web/src/api/types.ts (5)
  • PaginationParams (552-555)
  • Message (447-458)
  • PaginatedResponse (105-107)
  • Channel (462-466)
  • ApiResponse (94-96)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrapPaginated (66-82)
  • unwrap (54-60)
web/src/stores/company.ts (3)
web/src/api/types.ts (2)
  • CompanyConfig (392-396)
  • Department (381-385)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/utils/constants.ts (1)
  • MAX_PAGE_SIZE (15-15)
web/src/api/endpoints/analytics.ts (2)
web/src/api/types.ts (2)
  • OverviewMetrics (372-377)
  • ApiResponse (94-96)
web/src/api/client.ts (2)
  • apiClient (12-16)
  • unwrap (54-60)
web/src/main.ts (2)
web/src/router/index.ts (1)
  • router (34-34)
web/src/primevue-preset.ts (1)
  • primeVueOptions (4-12)
web/src/composables/useOptimisticUpdate.ts (1)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/composables/useAuth.ts (1)
web/src/utils/constants.ts (1)
  • WRITE_ROLES (52-52)
web/src/stores/providers.ts (2)
web/src/api/types.ts (1)
  • ProviderConfig (414-418)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/api/endpoints/health.ts (2)
web/src/api/types.ts (2)
  • HealthStatus (470-476)
  • ApiResponse (94-96)
web/src/api/client.ts (2)
  • apiClient (12-16)
  • unwrap (54-60)
web/src/api/endpoints/auth.ts (1)
web/src/api/client.ts (2)
  • apiClient (12-16)
  • unwrap (54-60)
web/src/stores/budget.ts (2)
web/src/api/types.ts (4)
  • CostRecord (331-341)
  • BudgetConfig (356-363)
  • AgentSpending (365-368)
  • WsEvent (519-524)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/__tests__/stores/auth.test.ts (1)
web/src/stores/auth.ts (1)
  • useAuthStore (8-165)
web/src/__tests__/stores/budget.test.ts (2)
web/src/api/types.ts (2)
  • CostRecord (331-341)
  • WsEvent (519-524)
web/src/stores/budget.ts (1)
  • useBudgetStore (16-82)
web/src/__tests__/stores/tasks.test.ts (2)
web/src/api/types.ts (2)
  • Task (152-178)
  • WsEvent (519-524)
web/src/stores/tasks.ts (1)
  • useTaskStore (15-150)
web/src/api/client.ts (1)
web/src/api/types.ts (2)
  • ApiResponse (94-96)
  • PaginatedResponse (105-107)
web/src/stores/agents.ts (3)
web/src/api/types.ts (2)
  • AgentConfig (313-327)
  • WsEvent (519-524)
web/src/utils/constants.ts (1)
  • MAX_PAGE_SIZE (15-15)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/__tests__/utils/errors.test.ts (1)
web/src/utils/errors.ts (2)
  • isAxiosError (8-10)
  • getErrorMessage (16-66)
web/src/__tests__/composables/usePolling.test.ts (1)
web/src/composables/usePolling.ts (1)
  • usePolling (9-56)
web/src/__tests__/router/guards.test.ts (2)
web/src/router/guards.ts (1)
  • authGuard (12-36)
web/src/stores/auth.ts (1)
  • useAuthStore (8-165)
web/src/api/endpoints/approvals.ts (2)
web/src/api/types.ts (1)
  • ApprovalFilters (257-263)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrapPaginated (66-82)
  • unwrap (54-60)
web/src/api/endpoints/tasks.ts (2)
web/src/api/types.ts (8)
  • TaskFilters (211-217)
  • PaginatedResponse (105-107)
  • Task (152-178)
  • ApiResponse (94-96)
  • CreateTaskRequest (180-190)
  • UpdateTaskRequest (192-199)
  • TransitionTaskRequest (201-205)
  • CancelTaskRequest (207-209)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrapPaginated (66-82)
  • unwrap (54-60)
web/src/__tests__/api/client.test.ts (1)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrap (54-60)
  • unwrapPaginated (66-82)
web/src/utils/constants.ts (1)
web/src/api/types.ts (1)
  • TaskStatus (5-14)
web/src/__tests__/stores/approvals.test.ts (2)
web/src/api/types.ts (2)
  • ApprovalItem (221-236)
  • WsEvent (519-524)
web/src/stores/approvals.ts (1)
  • useApprovalStore (7-91)
web/src/__tests__/utils/constants.test.ts (1)
web/src/utils/constants.ts (4)
  • TASK_STATUS_ORDER (23-33)
  • TERMINAL_STATUSES (36-36)
  • VALID_TRANSITIONS (39-49)
  • NAV_ITEMS (55-66)
web/src/stores/analytics.ts (2)
web/src/api/types.ts (1)
  • OverviewMetrics (372-377)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/stores/approvals.ts (2)
web/src/api/types.ts (5)
  • ApprovalItem (221-236)
  • ApprovalFilters (257-263)
  • ApproveRequest (249-251)
  • RejectRequest (253-255)
  • WsEvent (519-524)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/stores/auth.ts (4)
web/src/api/types.ts (2)
  • UserInfoResponse (133-138)
  • HumanRole (46-51)
web/src/router/index.ts (1)
  • router (34-34)
web/src/api/endpoints/auth.ts (3)
  • setup (11-14)
  • login (16-19)
  • changePassword (21-24)
web/src/utils/errors.ts (2)
  • isAxiosError (8-10)
  • getErrorMessage (16-66)
web/src/router/index.ts (1)
web/src/router/guards.ts (1)
  • authGuard (12-36)
web/src/__tests__/stores/websocket.test.ts (2)
web/src/stores/websocket.ts (1)
  • useWebSocketStore (23-229)
web/src/api/types.ts (1)
  • WsEvent (519-524)
web/src/api/endpoints/budget.ts (2)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrap (54-60)
  • unwrapPaginated (66-82)
web/src/api/types.ts (1)
  • PaginationParams (552-555)
web/src/__tests__/composables/useOptimisticUpdate.test.ts (1)
web/src/composables/useOptimisticUpdate.ts (1)
  • useOptimisticUpdate (18-50)
web/src/api/endpoints/providers.ts (1)
web/src/api/client.ts (2)
  • apiClient (12-16)
  • unwrap (54-60)
web/src/stores/tasks.ts (3)
web/src/api/types.ts (7)
  • Task (152-178)
  • TaskFilters (211-217)
  • CreateTaskRequest (180-190)
  • UpdateTaskRequest (192-199)
  • TransitionTaskRequest (201-205)
  • CancelTaskRequest (207-209)
  • WsEvent (519-524)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/api/endpoints/tasks.ts (4)
  • createTask (23-26)
  • updateTask (28-31)
  • transitionTask (33-36)
  • cancelTask (38-41)
🪛 Biome (2.4.6)
web/src/main.ts

[error] 23-23: Unexpected control character in a regular expression.

(lint/suspicious/noControlCharactersInRegex)


[error] 23-23: Unexpected control character in a regular expression.

(lint/suspicious/noControlCharactersInRegex)


[error] 25-25: Unexpected control character in a regular expression.

(lint/suspicious/noControlCharactersInRegex)


[error] 25-25: Unexpected control character in a regular expression.

(lint/suspicious/noControlCharactersInRegex)

web/src/styles/global.css

[error] 3-34: Tailwind-specific syntax is disabled.

(parse)

web/src/stores/websocket.ts

[error] 8-8: Unexpected control character in a regular expression.

(lint/suspicious/noControlCharactersInRegex)


[error] 8-8: Unexpected control character in a regular expression.

(lint/suspicious/noControlCharactersInRegex)

🪛 GitHub Check: CodeQL
web/src/stores/websocket.ts

[warning] 91-91: Log injection
Log entry depends on a user-provided value.


[warning] 99-99: Log injection
Log entry depends on a user-provided value.

🪛 GitHub Check: Dashboard Lint
web/src/stores/providers.ts

[warning] 16-16:
Generic Object Injection Sink

web/src/stores/websocket.ts

[warning] 189-189:
Generic Object Injection Sink


[warning] 184-184:
Generic Object Injection Sink


[warning] 17-17:
Generic Object Injection Sink


[warning] 17-17:
Generic Object Injection Sink

web/src/api/endpoints/providers.ts

[warning] 15-15:
Generic Object Injection Sink

🪛 LanguageTool
docs/user_guide.md

[style] ~21-~21: Using many exclamation marks might seem excessive (in this case: 3 exclamation marks for a text that’s 1272 characters long)
Context: ...he UI is planned for a future release. !!! info "Active Development" SynthOrg ...

(EN_EXCESSIVE_EXCLAMATION)

docs/design/operations.md

[grammar] ~969-~969: Please add a punctuation mark at the end of paragraph.
Context: ...# Web UI Features !!! info "In Progress" The Web UI is being built as a Vue...

(PUNCTUATION_PARAGRAPH_END)

🪛 markdownlint-cli2 (0.21.0)
docs/design/operations.md

[warning] 971-971: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

🪛 Stylelint (17.4.0)
web/src/styles/global.css

[error] 3-3: Unexpected unknown at-rule "@theme" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

CLAUDE.md Outdated
stores/ # Pinia stores (auth, agents, tasks, budget, messages, approvals, websocket, analytics, company, providers)
styles/ # Global CSS and PrimeVue theme configuration
utils/ # Constants, formatters, error helpers
views/ # Page-level components (Dashboard, TaskBoard, AgentProfiles, BudgetPanel, etc.)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Scope drift in views list for this PR.

Line 125 lists full feature pages (Dashboard, TaskBoard, etc.), but this PR is scoped to core infrastructure with pages deferred to PR 2. Please narrow this line to current in-repo views for this PR.

📝 Suggested wording
-    views/        # Page-level components (Dashboard, TaskBoard, AgentProfiles, BudgetPanel, etc.)
+    views/        # Page-level components currently included in PR 1 (Login, Setup, PlaceholderHome); feature pages land in PR 2
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
views/ # Page-level components (Dashboard, TaskBoard, AgentProfiles, BudgetPanel, etc.)
views/ # Page-level components currently included in PR 1 (Login, Setup, PlaceholderHome); feature pages land in PR 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 125, Narrow the scope of the inline examples on the
"views/" line by removing the deferred full-feature page names (Dashboard,
TaskBoard, AgentProfiles, BudgetPanel) and replace them with only the view
components actually added in this PR or a neutral phrase like "Page-level
components (current PR views only)"; edit the comment on the line that contains
"views/" to either list the specific in-repo view names introduced by this PR or
remove the example list altogether so it no longer claims pages that are
deferred to PR 2.

Comment on lines +33 to 45
location /api/v1/ws {
proxy_pass http://backend:8000;
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 3600s;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and examine the nginx.conf file
find . -name "nginx.conf" -type f

Repository: Aureliolo/synthorg

Length of output: 78


🏁 Script executed:

# Read the nginx.conf file to see the context
cat web/nginx.conf

Repository: Aureliolo/synthorg

Length of output: 2411


🏁 Script executed:

# Get line counts to ensure we understand the file structure
wc -l web/nginx.conf

Repository: Aureliolo/synthorg

Length of output: 79


Preserve the original X-Forwarded-Proto header to maintain the client's protocol in TLS-terminated deployments.

This server only listens on plain HTTP/8080, so $scheme is always http. When deployed behind a TLS-terminating reverse proxy or load balancer, overwriting X-Forwarded-Proto with $scheme strips the original client protocol before the request reaches the backend. This causes the backend to incorrectly believe requests were insecure, breaking secure redirects, absolute URL generation, and scheme-aware handling. Preserve the incoming header with an http-scope fallback instead.

Suggested nginx change
+# http {} scope
+map $http_x_forwarded_proto $forwarded_proto {
+    default $http_x_forwarded_proto;
+    ''      $scheme;
+}
+
 server {
@@
-        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-Proto $forwarded_proto;
@@
-        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-Proto $forwarded_proto;

Applies to both /api/v1/ws (lines 33–45) and /api/ (lines 48–57) location blocks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/nginx.conf` around lines 33 - 45, The X-Forwarded-Proto header is being
overwritten with $scheme (which is always "http" here) — change this to preserve
an incoming header with an http-scope fallback by introducing a map (e.g. map
$http_x_forwarded_proto $x_forwarded_proto { default $http_x_forwarded_proto; ''
$scheme; }) and then set proxy_set_header X-Forwarded-Proto $x_forwarded_proto
in both the location /api/v1/ws and location /api/ blocks so the original client
protocol is preserved when present and $scheme is used only as a fallback.

Comment on lines +7 to +13
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint src/",
"type-check": "vue-tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "web/package.json direct vite declaration:"
jq -r '.dependencies.vite // .devDependencies.vite // "MISSING"' web/package.json

echo
echo "Other package manifests that declare vite:"
fd '^package\.json$' . -x sh -c '
  dep=$(jq -r ".dependencies.vite // .devDependencies.vite // empty" "$1")
  if [ -n "$dep" ]; then
    printf "%s\t%s\n" "$1" "$dep"
  fi
' sh {}

Repository: Aureliolo/synthorg

Length of output: 155


🏁 Script executed:

sed -n '31,45p' web/package.json

Repository: Aureliolo/synthorg

Length of output: 498


Add vite as a direct devDependency.

Scripts for dev, build, and preview invoke the Vite CLI, but vite is not declared in devDependencies. While vitest and @vitejs/plugin-vue may pull vite as a transitive dependency, relying on this for CLI tools is brittle across different package managers and dependency resolution. Declare vite explicitly in the devDependencies block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/package.json` around lines 7 - 13, The package.json scripts ("dev",
"build", "preview") call the Vite CLI but Vite is not declared in
devDependencies; add an explicit devDependency entry for "vite" (e.g., by
running the package manager command to install it as a devDependency or by
adding "vite": "<compatible-version>" under devDependencies) so the CLI is
guaranteed to be available; ensure the version you choose is compatible with
existing dev tools like "vitest" and "@vitejs/plugin-vue" to avoid resolution
conflicts.

Comment on lines +28 to +33
const handlers = (apiClient.interceptors.request as unknown as { handlers: Array<{ fulfilled?: (c: InternalAxiosRequestConfig) => InternalAxiosRequestConfig }> }).handlers
const interceptor = handlers?.[0]?.fulfilled
if (interceptor) {
const result = interceptor(config)
expect(result.headers.get('Authorization')).toBe('Bearer test-jwt-token')
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the test file and inspect it
find . -name "client.test.ts" -path "*/web/src/__tests__/*" 2>/dev/null | head -5

Repository: Aureliolo/synthorg

Length of output: 100


🏁 Script executed:

# Check the structure of web/src
ls -la web/src/ 2>/dev/null | head -20

Repository: Aureliolo/synthorg

Length of output: 880


🏁 Script executed:

# Get a broader view of test files
fd "client\.test\.ts" web/

Repository: Aureliolo/synthorg

Length of output: 98


🏁 Script executed:

# Read the test file to inspect lines 28-43
wc -l web/src/__tests__/api/client.test.ts

Repository: Aureliolo/synthorg

Length of output: 102


🏁 Script executed:

# Now let's read the specific lines around 28-43
sed -n '20,50p' web/src/__tests__/api/client.test.ts

Repository: Aureliolo/synthorg

Length of output: 1304


🏁 Script executed:

# Also check the client.ts interceptor setup
cat web/src/api/client.ts

Repository: Aureliolo/synthorg

Length of output: 2703


🏁 Script executed:

# Check if there are any Axios type definitions or documentation in the repo
fd -e d.ts -e ts package.json 2>/dev/null | head -5

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Check package.json to see Axios version
grep -A 2 -B 2 '"axios"' web/package.json

Repository: Aureliolo/synthorg

Length of output: 179


🏁 Script executed:

# Read the entire test file to understand the context and structure
cat web/src/__tests__/api/client.test.ts

Repository: Aureliolo/synthorg

Length of output: 3939


Assert interceptor existence instead of silently passing when lookup fails.

Tests gate assertions inside if (interceptor), so they pass without any checks if the interceptor is not registered or Axios's internal handlers array changes shape. This creates false confidence in a security-critical JWT auth path. Assert the interceptor is a function before invoking it:

Minimal test hardening
 const interceptor = handlers?.[0]?.fulfilled
- if (interceptor) {
-   const result = interceptor(config)
-   expect(result.headers.get('Authorization')).toBe('Bearer test-jwt-token')
- }
+ expect(interceptor).toBeTypeOf('function')
+ const result = interceptor!(config)
+ expect(result.headers.get('Authorization')).toBe('Bearer test-jwt-token')

Apply to both test cases (lines 28-33 and 38-43).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handlers = (apiClient.interceptors.request as unknown as { handlers: Array<{ fulfilled?: (c: InternalAxiosRequestConfig) => InternalAxiosRequestConfig }> }).handlers
const interceptor = handlers?.[0]?.fulfilled
if (interceptor) {
const result = interceptor(config)
expect(result.headers.get('Authorization')).toBe('Bearer test-jwt-token')
}
const handlers = (apiClient.interceptors.request as unknown as { handlers: Array<{ fulfilled?: (c: InternalAxiosRequestConfig) => InternalAxiosRequestConfig }> }).handlers
const interceptor = handlers?.[0]?.fulfilled
expect(interceptor).toBeTypeOf('function')
const result = interceptor!(config)
expect(result.headers.get('Authorization')).toBe('Bearer test-jwt-token')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/api/client.test.ts` around lines 28 - 33, The test
currently guards the interceptor call with "if (interceptor)" which can silently
skip assertions; instead assert the interceptor exists and is a function before
invoking it: add an explicit expect(typeof interceptor).toBe('function') (or
similar) targeting the interceptor variable extracted from
apiClient.interceptors.request.handlers so the test fails if the request
interceptor is not registered; apply this change to both test cases that extract
"handlers" / "interceptor" (the blocks around apiClient.interceptors.request and
the subsequent config invocation).

Comment on lines +49 to +55
it('does not render action container when slot is empty', () => {
const wrapper = mount(EmptyState, {
props: { title: 'Empty' },
})
// The mt-4 div should not render when no action slot is provided
const actionDivs = wrapper.findAll('.mt-4')
expect(actionDivs).toHaveLength(0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the test file
fd -t f "EmptyState.test.ts" | head -5

Repository: Aureliolo/synthorg

Length of output: 109


🏁 Script executed:

# Also find the EmptyState component
fd -t f "EmptyState.ts" -o "EmptyState.vue" | head -10

Repository: Aureliolo/synthorg

Length of output: 209


🏁 Script executed:

# Search for EmptyState component file properly
fd -t f -i emptystate

Repository: Aureliolo/synthorg

Length of output: 150


🏁 Script executed:

# Read the test file to see the actual test code
cat -n web/src/__tests__/components/EmptyState.test.ts

Repository: Aureliolo/synthorg

Length of output: 2287


🏁 Script executed:

# Read the EmptyState component to understand how slots are handled
cat -n web/src/components/common/EmptyState.vue

Repository: Aureliolo/synthorg

Length of output: 777


Test name contradicts actual behavior: this tests "slot not provided", not "slot is empty".

At line 49, the test name claims to verify empty slot behavior, but no slot is passed at all. The component's v-if="$slots.action" check (line 18 of EmptyState.vue) will return false in both cases—when the slot is not provided and when provided but empty—but they represent different scenarios. The current test only covers the former and misses validating the case where action slot is provided but resolves to no content.

Suggested test adjustment
-  it('does not render action container when slot is empty', () => {
+  it('does not render action container when action slot resolves to empty content', () => {
     const wrapper = mount(EmptyState, {
       props: { title: 'Empty' },
+      slots: { action: () => [] },
     })
-    // The mt-4 div should not render when no action slot is provided
     const actionDivs = wrapper.findAll('.mt-4')
     expect(actionDivs).toHaveLength(0)
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/components/EmptyState.test.ts` around lines 49 - 55, Rename
the existing test in EmptyState.test.ts to "does not render action container
when slot is not provided" to match its behavior, and add a new test that mounts
the EmptyState component with an explicitly provided but empty action slot
(e.g., pass slots: { action: '' } or a slot function that returns null) to
assert the '.mt-4' action container is not rendered; reference the EmptyState
component and the test file's existing
expect(wrapper.findAll('.mt-4')).toHaveLength(0) assertion to validate both
scenarios.

Comment on lines +55 to +66
export const NAV_ITEMS = [
{ label: 'Dashboard', icon: 'pi pi-home', to: '/' },
{ label: 'Org Chart', icon: 'pi pi-sitemap', to: '/org-chart' },
{ label: 'Tasks', icon: 'pi pi-check-square', to: '/tasks' },
{ label: 'Messages', icon: 'pi pi-comments', to: '/messages' },
{ label: 'Approvals', icon: 'pi pi-shield', to: '/approvals' },
{ label: 'Agents', icon: 'pi pi-users', to: '/agents' },
{ label: 'Budget', icon: 'pi pi-chart-bar', to: '/budget' },
{ label: 'Meetings', icon: 'pi pi-video', to: '/meetings' },
{ label: 'Artifacts', icon: 'pi pi-file', to: '/artifacts' },
{ label: 'Settings', icon: 'pi pi-cog', to: '/settings' },
] as const
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Router-related files:"
fd '(router|routes)' -t f

echo
echo "Registered nav paths (absolute or nested child paths):"
rg -n --type ts --type vue "path:\s*['\"]/?(org-chart|tasks|messages|approvals|agents|budget|meetings|artifacts|settings)['\"]"

Repository: Aureliolo/synthorg

Length of output: 327


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Web directory structure ==="
find web/src -type f -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.vue" 2>/dev/null | head -30

echo
echo "=== Search for router files in web/src ==="
fd '(router|routes|app)' web/src -t f 2>/dev/null

echo
echo "=== Search for path definitions in web/src ==="
rg -n "path\s*[:=]" web/src --type ts -A 2 2>/dev/null | head -40

echo
echo "=== Search for route patterns in web/src ==="
rg -n "to:\s*['\"]/" web/src --type ts 2>/dev/null | head -20

Repository: Aureliolo/synthorg

Length of output: 4376


🏁 Script executed:

cat -n web/src/router/index.ts

Repository: Aureliolo/synthorg

Length of output: 1033


Don't surface feature routes before the router can handle them.

The router only registers /login, /setup, and /. The catch-all pattern /:pathMatch(.*)* redirects to /, which means clicking nav items for /org-chart, /tasks, /messages, /approvals, /agents, /budget, /meetings, /artifacts, or /settings will silently redirect back to home. Either hide these entries from NAV_ITEMS until PR 2 lands or register temporary placeholder routes for each path now.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/constants.ts` around lines 55 - 66, NAV_ITEMS currently exposes
links for routes the router doesn't register yet (only /login, /setup and /
exist), causing clicks to be caught by the catch-all redirect; either remove or
comment out the unreleased entries in NAV_ITEMS (Org Chart, Tasks, Messages,
Approvals, Agents, Budget, Meetings, Artifacts, Settings) until the router
supports them, or alternatively add temporary placeholder routes for each path
in your router setup so those paths resolve instead of redirecting—update
NAV_ITEMS or the router consistently so navigation items only point to
registered routes.

Comment on lines +55 to +61
if (error instanceof Error) {
// Only surface messages from errors explicitly thrown by our own code.
// Errors from unknown sources could contain backend internals.
const msg = error.message
if (msg && msg.length < 200 && !/^\{/.test(msg)) {
return msg
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Plain Error messages still bypass the backend-error sanitization.

web/src/api/client.ts Lines 53-59 and Lines 65-81 throw new Error(body.error) when an API envelope reports success: false. Those failures land here as plain Errors, and Lines 58-60 currently echo any short message verbatim, so a backend stack trace or SQL error wrapped in a 200 response will still reach the UI. Only pass through messages from an explicit safe frontend error type; everything else should fall back to the generic message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/errors.ts` around lines 55 - 61, The current check allows any
plain Error through (error instanceof Error) which lets backend messages leak;
change the logic to only pass through messages when the error is an explicit
frontend-safe type (e.g. FrontendError or SafeFrontendError) and reject plain
Error instances. Update the condition around error instanceof Error to instead
check for the safe class (e.g. error instanceof FrontendError) and keep the
existing msg length/format guards; for all other Error instances return the
generic sanitized message. If FrontendError doesn't exist yet, add a simple
exported class (or type guard) used by client code when throwing safe errors,
and use that class name in the check within the function.

Comment on lines +60 to +68
export function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const mins = Math.floor((seconds % 3600) / 60)
const parts: string[] = []
if (days > 0) parts.push(`${days}d`)
if (hours > 0) parts.push(`${hours}h`)
if (mins > 0 || parts.length === 0) parts.push(`${mins}m`)
return parts.join(' ')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard invalid uptime inputs before formatting.

NaN currently renders as NaNm, and negative inputs can produce -1m / negative day values because the breakdown uses Math.floor directly. Return a safe fallback or clamp to 0 before computing the parts.

💡 Proposed fix
 export function formatUptime(seconds: number): string {
+  if (!Number.isFinite(seconds) || seconds < 0) return '—'
   const days = Math.floor(seconds / 86400)
   const hours = Math.floor((seconds % 86400) / 3600)
   const mins = Math.floor((seconds % 3600) / 60)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/format.ts` around lines 60 - 68, formatUptime currently allows
NaN and negative inputs which produce invalid output like "NaNm" or "-1m";
update the formatUptime function to validate and clamp its input by first
checking Number.isFinite(seconds) and seconds >= 0 (or coerce any invalid value
to 0) before computing days/hours/mins, then proceed with the existing breakdown
and return; this ensures the output is never negative or contains "NaN" and will
produce a safe fallback like "0m" for bad inputs.

Comment on lines +21 to +58
// Reactive clock so `locked` re-evaluates when lockout expires
const now = ref(Date.now())
const clockTimer = setInterval(() => { now.value = Date.now() }, 1000)
onUnmounted(() => clearInterval(clockTimer))

const locked = computed(() => !!(lockedUntil.value && now.value < lockedUntil.value))

function checkAndClearLockout(): boolean {
if (lockedUntil.value && Date.now() >= lockedUntil.value) {
lockedUntil.value = null
attempts.value = 0
}
return locked.value
}

async function handleLogin() {
if (checkAndClearLockout()) {
error.value = 'Too many failed attempts. Please wait before trying again.'
return
}
error.value = null
try {
await auth.login(username.value, password.value)
attempts.value = 0
if (auth.mustChangePassword) {
toast.add({ severity: 'warn', summary: 'Password change required', life: 5000 })
}
router.push('/')
} catch (err) {
attempts.value++
if (attempts.value >= LOGIN_MAX_ATTEMPTS) {
lockedUntil.value = Date.now() + LOGIN_LOCKOUT_MS
attempts.value = 0
error.value = `Too many failed attempts. Please wait ${LOGIN_LOCKOUT_MS / 1000} seconds.`
} else {
error.value = getErrorMessage(err)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Derive lockout state from actual auth failures, not any thrown error.

attempts is incremented for network/5xx failures too, and the lockout text lives in error, so the alert can remain visible after lockedUntil expires. This same block is duplicated in web/src/views/SetupPage.vue, which makes the behavior easy to drift. Please move this into a shared composable that only counts credential failures and derives the lockout message from lockedUntil / now.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/views/LoginPage.vue` around lines 21 - 58, Extract the lockout logic
into a shared composable (e.g., useLockout) that centralizes now/clockTimer,
locked (computed from lockedUntil and now), checkAndClearLockout, attempts and
lockedUntil and exposes functions: isLocked(), recordCredentialFailure(), and
clearFailures(); update handleLogin to only call recordCredentialFailure() when
auth.login fails with a credential error (not on network/5xx — detect via
getErrorMessage or error type), and derive the visible error text from
isLocked()/lockedUntil+now rather than setting error.value directly on lock
creation; replace duplicated blocks in LoginPage.vue and SetupPage.vue to call
the composable APIs (use isLocked() to short-circuit and to render the lockout
message, and recordCredentialFailure() on confirmed bad-credentials).

Comment on lines +2 to +3
<div class="flex items-center justify-center h-full text-slate-400">
Dashboard loading…
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add live-region semantics for the loading placeholder.

Line 2/Line 3 currently render passive text only; adding status semantics improves screen-reader feedback during route load.

Suggested patch
-  <div class="flex items-center justify-center h-full text-slate-400">
+  <div class="flex items-center justify-center h-full text-slate-400" role="status" aria-live="polite">
     Dashboard loading…
   </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="flex items-center justify-center h-full text-slate-400">
Dashboard loading…
<div class="flex items-center justify-center h-full text-slate-400" role="status" aria-live="polite">
Dashboard loading…
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/views/PlaceholderHome.vue` around lines 2 - 3, The loading
placeholder div in PlaceholderHome.vue currently renders passive text; add ARIA
live-region semantics by updating the same div (the element with class "flex
items-center justify-center h-full text-slate-400") to include live-region
attributes such as role="status" and aria-live="polite" (optionally
aria-busy="true" while loading) so screen readers announce the "Dashboard
loading…" message during route loads.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive and well-architected foundation for a new web dashboard. The code quality is generally very high, with strong attention to security, robustness, and maintainability, particularly in the API client, Pinia stores, and utility functions. The use of multi-stage Docker builds, security headers, and detailed test suites is commendable. However, there is a critical issue in web/package.json where many dependency versions are incorrect and will prevent the project from installing. There is also a medium-severity security concern in the Nginx configuration regarding the Content Security Policy.

Comment on lines +15 to +46
"dependencies": {
"@primevue/themes": "^4",
"@tailwindcss/vite": "^4",
"@vue-flow/controls": "^1",
"@vue-flow/core": "^1",
"@vue-flow/minimap": "^1",
"axios": "^1",
"echarts": "^5",
"pinia": "^2",
"primevue": "^4",
"tailwindcss": "^4",
"vue": "^3.5",
"vue-draggable-plus": "^0.6",
"vue-echarts": "^7",
"vue-router": "^4"
},
"devDependencies": {
"@types/node": "^25.5.0",
"@typescript-eslint/parser": "^8.57.0",
"@vitejs/plugin-vue": "^5",
"@vitest/coverage-v8": "^3.2.4",
"@vue/test-utils": "^2",
"@vue/tsconfig": "^0.7",
"eslint": "^9",
"eslint-plugin-security": "^4.0.0",
"eslint-plugin-vue": "^9",
"jsdom": "^26",
"typescript": "^5.7",
"typescript-eslint": "^8.57.0",
"vitest": "^3",
"vue-tsc": "^2"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Many of the package versions in dependencies and devDependencies are invalid or refer to non-existent versions, which will cause npm install to fail. For example, vue version 3.5 is not yet released, and @types/node version 25.x does not exist.

I've corrected the versions to the latest stable releases (or alpha/beta where appropriate, like for Tailwind CSS v4). This should allow the dependencies to be installed correctly.

  "dependencies": {
    "@primevue/themes": "^4.0.0",
    "@tailwindcss/vite": "^4.0.0-alpha.13",
    "@vue-flow/controls": "^1.1.2",
    "@vue-flow/core": "^1.33.5",
    "@vue-flow/minimap": "^1.3.0",
    "axios": "^1.7.2",
    "echarts": "^5.5.0",
    "pinia": "^2.1.7",
    "primevue": "^4.0.0",
    "tailwindcss": "^4.0.0-alpha.13",
    "vue": "^3.4.29",
    "vue-draggable-plus": "^0.6.0",
    "vue-echarts": "^7.0.0-alpha.2",
    "vue-router": "^4.4.0"
  },
  "devDependencies": {
    "@types/node": "^20.14.9",
    "@typescript-eslint/parser": "^7.13.0",
    "@vitejs/plugin-vue": "^5.0.5",
    "@vitest/coverage-v8": "^1.6.0",
    "@vue/test-utils": "^2.4.6",
    "@vue/tsconfig": "^0.7.0",
    "eslint": "^9.5.0",
    "eslint-plugin-security": "^3.0.0",
    "eslint-plugin-vue": "^9.26.0",
    "jsdom": "^24.1.0",
    "typescript": "^5.5.2",
    "vitest": "^1.6.0",
    "vue-tsc": "^2.0.21"
  }

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; font-src 'self'" always;
# style-src 'unsafe-inline' required by PrimeVue which injects dynamic inline styles.
# connect-src 'self' covers same-origin ws:/wss: in modern browsers (Chrome 73+, Firefox 45+).
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'self'" always;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

Your Content Security Policy (CSP) allows style-src 'unsafe-inline', which is necessary for some UI libraries like PrimeVue that inject styles dynamically. While the comment explains the reason, this does reduce protection against certain Cross-Site Scripting (XSS) attacks.

For enhanced security, you could consider using CSP nonces. This involves generating a unique nonce on the server for each request, adding it to the CSP header, and applying it as an attribute to all script and style tags. While more complex to implement, especially with a statically generated frontend, it would allow you to remove 'unsafe-inline'.

- Fix ESM __dirname usage in vite/vitest configs (fileURLToPath)
- Add explicit vite devDependency to package.json
- Nginx: preserve X-Forwarded-Proto, suppress WS access logs
- Sanitize log output with charCode filter (avoid control char regex)
- Add NaN/negative guard to formatUptime
- Use import.meta.env.BASE_URL for subpath deployment support
- Restructure optimistic update to capture rollback before mutation
- Prototype pollution prevention in providers endpoint
- Runtime payload validation in agents, approvals, tasks WS handlers
- Stale response prevention in messages store (request ID counter)
- Pagination loop for company departments fetch
- Budget store respects active filters in WS handler
- Approvals store tracks active query for WS insertion
- ARIA attributes on ConnectionStatus, Topbar, PlaceholderHome
- Credential vs network error distinction for login lockout
- Fix test mocks to match actual TypeScript interfaces
- Add edge-case tests (dedup, malformed payload, slot rendering)
- TODO(#346) for mustChangePassword router enforcement
}

if (msg.error) {
console.error('WebSocket error:', sanitizeLogValue(msg.error))

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI 22 days ago

In general, to fix log injection you must ensure that any user-controlled value is sanitized/encoded before being written to logs, removing or neutralizing characters that change log structure (newlines, control chars) and, when logs may be viewed as HTML, encoding HTML-significant characters. In this case, there is already a centralized sanitizeLogValue helper used wherever untrusted values are logged, so the best fix is to harden this function rather than changing each call site.

Concretely, in web/src/stores/websocket.ts, lines 6–15 define sanitizeLogValue. We should extend it so that in addition to stripping control characters and truncating to max characters, it also HTML-escapes characters like &, <, >, ", and '. This ensures that even if logs are rendered into a web UI, user input cannot inject markup or spoof entries. The rest of the logic (stringifying the value, filtering characters, truncating) can remain unchanged, preserving functionality. No new imports are needed; we can implement HTML escaping inline with a small replacement map.

Suggested changeset 1
web/src/stores/websocket.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/web/src/stores/websocket.ts b/web/src/stores/websocket.ts
--- a/web/src/stores/websocket.ts
+++ b/web/src/stores/websocket.ts
@@ -3,7 +3,7 @@
 import type { WsChannel, WsEvent, WsEventHandler } from '@/api/types'
 import { WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY, WS_MAX_RECONNECT_ATTEMPTS } from '@/utils/constants'
 
-/** Strip all control characters and truncate for safe logging. */
+/** Strip all control characters, truncate, and HTML-escape for safe logging. */
 function sanitizeLogValue(value: unknown, max = 200): string {
   const raw = String(value)
   let result = ''
@@ -12,7 +12,17 @@
     result += (code >= 0x20 && code !== 0x7f) ? ch : ' '
     if (result.length >= max) break
   }
-  return result
+  // HTML-escape to keep logs safe if rendered in a browser-based viewer.
+  return result.replace(/[&<>"']/g, (c) => {
+    switch (c) {
+      case '&': return '&amp;'
+      case '<': return '&lt;'
+      case '>': return '&gt;'
+      case '"': return '&quot;'
+      case '\'': return '&#39;'
+      default: return c
+    }
+  })
 }
 
 /** Build a stable deduplication key for a subscription (sorted channels + sorted filter keys). */
EOF
@@ -3,7 +3,7 @@
import type { WsChannel, WsEvent, WsEventHandler } from '@/api/types'
import { WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY, WS_MAX_RECONNECT_ATTEMPTS } from '@/utils/constants'

/** Strip all control characters and truncate for safe logging. */
/** Strip all control characters, truncate, and HTML-escape for safe logging. */
function sanitizeLogValue(value: unknown, max = 200): string {
const raw = String(value)
let result = ''
@@ -12,7 +12,17 @@
result += (code >= 0x20 && code !== 0x7f) ? ch : ' '
if (result.length >= max) break
}
return result
// HTML-escape to keep logs safe if rendered in a browser-based viewer.
return result.replace(/[&<>"']/g, (c) => {
switch (c) {
case '&': return '&amp;'
case '<': return '&lt;'
case '>': return '&gt;'
case '"': return '&quot;'
case '\'': return '&#39;'
default: return c
}
})
}

/** Build a stable deduplication key for a subscription (sorted channels + sorted filter keys). */
Copilot is powered by AI and may make mistakes. Always verify output.
try {
dispatchEvent(msg as unknown as WsEvent)
} catch (handlerErr) {
console.error('WebSocket event handler error:', sanitizeLogValue(handlerErr), 'Event type:', sanitizeLogValue(msg.event_type, 100))

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI 22 days ago

In general, to fix log injection when logging user input, remove or neutralize control characters (especially newlines) and ensure that user-controlled segments are clearly delimited so they cannot masquerade as full log entries. In this file, control characters are already stripped by sanitizeLogValue, but the user-provided value is interpolated into the log without any clear quoting or prefixing that marks it as user input. The best minimal fix is to adjust the specific log message on line 106 so that the event type from the server is clearly labeled and/or quoted, rather than appended as a bare value. This avoids confusion in logs and aligns with the recommendation that “user input is clearly marked in log entries.”

Concretely, in web/src/stores/websocket.ts, within the socket.onmessage handler, change the console.error on line 106 to wrap the event type with a clear label or brackets. For example, instead of passing 'Event type:', sanitizeLogValue(msg.event_type, 100) as separate arguments, we can build a single message string where the event type appears as event_type="<sanitized>". No new imports or helpers are required; we reuse sanitizeLogValue. No behavior of the application changes aside from the exact shape of that log message.

Suggested changeset 1
web/src/stores/websocket.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/web/src/stores/websocket.ts b/web/src/stores/websocket.ts
--- a/web/src/stores/websocket.ts
+++ b/web/src/stores/websocket.ts
@@ -103,7 +103,13 @@
         try {
           dispatchEvent(msg as unknown as WsEvent)
         } catch (handlerErr) {
-          console.error('WebSocket event handler error:', sanitizeLogValue(handlerErr), 'Event type:', sanitizeLogValue(msg.event_type, 100))
+          console.error(
+            'WebSocket event handler error:',
+            sanitizeLogValue(handlerErr),
+            'Event type="',
+            sanitizeLogValue(msg.event_type, 100),
+            '"'
+          )
         }
       }
     }
EOF
@@ -103,7 +103,13 @@
try {
dispatchEvent(msg as unknown as WsEvent)
} catch (handlerErr) {
console.error('WebSocket event handler error:', sanitizeLogValue(handlerErr), 'Event type:', sanitizeLogValue(msg.event_type, 100))
console.error(
'WebSocket event handler error:',
sanitizeLogValue(handlerErr),
'Event type="',
sanitizeLogValue(msg.event_type, 100),
'"'
)
}
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 13, 2026 11:56 — with GitHub Actions Inactive
Comment on lines +31 to +45
(response: AxiosResponse) => response,
(error: AxiosError<{ error?: string; success?: boolean }>) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token')
localStorage.removeItem('auth_token_expires_at')
// Use router import for SPA-friendly navigation (preserves in-memory state)
if (window.location.pathname !== '/login' && window.location.pathname !== '/setup') {
// Dynamic import to avoid circular dependency with router -> stores -> api
import('@/router').then(({ router }) => {
router.push('/login')
}).catch(() => {
window.location.href = '/login'
})
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

401 interceptor doesn't sync Pinia auth state — auth guard redirects user back to /

The 401 handler clears localStorage and calls router.push('/login'), but the Pinia auth store's token ref is never cleared. When the navigation fires, authGuard evaluates auth.isAuthenticated which reads the still-truthy Pinia ref (!!token.value === true), sees the user as authenticated, and immediately calls next('/') — aborting the redirect to login.

The user ends up back at the dashboard in a broken state: the Pinia store thinks they are logged in, but every subsequent API request includes no Authorization header (because the request interceptor reads from localStorage, which is now empty), generating an endless stream of 401 errors until the page is hard-refreshed.

The clearAuth() path that actually fixes Pinia state is only triggered from fetchUser() (which only runs on initial auth or explicit call) — ordinary store mutations that hit 401 (e.g. a task fetch timing out) never get there.

The fix is to lazy-import the auth store inside the interceptor callback, the same pattern used for the router, to avoid circular dependency:

import('@/stores/auth').then(({ useAuthStore }) => {
  useAuthStore().clearAuth()
})

Or, alternatively, expose a lightweight event bus that clearAuth subscribes to, keeping the dependency graph clean.

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/api/client.ts
Line: 31-45

Comment:
**401 interceptor doesn't sync Pinia auth state — auth guard redirects user back to `/`**

The 401 handler clears `localStorage` and calls `router.push('/login')`, but the Pinia auth store's `token` ref is never cleared. When the navigation fires, `authGuard` evaluates `auth.isAuthenticated` which reads the still-truthy Pinia ref (`!!token.value === true`), sees the user as authenticated, and immediately calls `next('/')` — aborting the redirect to login.

The user ends up back at the dashboard in a broken state: the Pinia store thinks they are logged in, but every subsequent API request includes no `Authorization` header (because the request interceptor reads from `localStorage`, which is now empty), generating an endless stream of 401 errors until the page is hard-refreshed.

The `clearAuth()` path that actually fixes Pinia state is only triggered from `fetchUser()` (which only runs on initial auth or explicit call) — ordinary store mutations that hit 401 (e.g. a task fetch timing out) never get there.

The fix is to lazy-import the auth store inside the interceptor callback, the same pattern used for the router, to avoid circular dependency:

```ts
import('@/stores/auth').then(({ useAuthStore }) => {
  useAuthStore().clearAuth()
})
```

Or, alternatively, expose a lightweight event bus that `clearAuth` subscribes to, keeping the dependency graph clean.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +46 to +59
// Paginate until all departments are fetched
while (true) {
const result = await companyApi.listDepartments({ limit: MAX_PAGE_SIZE, offset })
if (gen !== departmentsGen) return // Stale request — abort
allDepts = [...allDepts, ...result.data]
if (allDepts.length >= result.total) break
offset += MAX_PAGE_SIZE
}
if (gen === departmentsGen) {
departments.value = allDepts
}
} catch (err) {
if (gen === departmentsGen) {
departmentsError.value = getErrorMessage(err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchDepartments can loop infinitely on an empty page with a non-zero total

If the backend returns { data: [], total: N } for any page (e.g. due to a race condition where items are deleted between pages, or a backend pagination bug), allDepts.length will never reach result.total, causing an unbounded loop that ties up the browser and the backend connection indefinitely.

Adding a guard on empty pages makes the loop unconditionally terminating:

Suggested change
// Paginate until all departments are fetched
while (true) {
const result = await companyApi.listDepartments({ limit: MAX_PAGE_SIZE, offset })
if (gen !== departmentsGen) return // Stale request — abort
allDepts = [...allDepts, ...result.data]
if (allDepts.length >= result.total) break
offset += MAX_PAGE_SIZE
}
if (gen === departmentsGen) {
departments.value = allDepts
}
} catch (err) {
if (gen === departmentsGen) {
departmentsError.value = getErrorMessage(err)
if (gen !== departmentsGen) return // Stale request — abort
allDepts = [...allDepts, ...result.data]
if (result.data.length === 0 || allDepts.length >= result.total) break
offset += MAX_PAGE_SIZE
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/stores/company.ts
Line: 46-59

Comment:
**`fetchDepartments` can loop infinitely on an empty page with a non-zero total**

If the backend returns `{ data: [], total: N }` for any page (e.g. due to a race condition where items are deleted between pages, or a backend pagination bug), `allDepts.length` will never reach `result.total`, causing an unbounded loop that ties up the browser and the backend connection indefinitely.

Adding a guard on empty pages makes the loop unconditionally terminating:

```suggestion
        if (gen !== departmentsGen) return // Stale request — abort
        allDepts = [...allDepts, ...result.data]
        if (result.data.length === 0 || allDepts.length >= result.total) break
        offset += MAX_PAGE_SIZE
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +65 to +76
messages.value = [...messages.value, message].slice(-MAX_WS_MESSAGES)
// Don't increment total — it tracks the server-side paginated count
// and is only set from the REST API response in fetchMessages()
}
}
}
}

return {
messages,
channels,
total,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing structural validation on WS message.sent payload

Every other store that handles WebSocket events has an explicit runtime type-guard before treating the payload as a domain type (isValidAgentPayload, isValidApprovalPayload, isValidTaskPayload, isCostRecord). This handler skips that step and directly casts event.payload to Message, then only guards on message.id.

If the payload is structurally malformed — for example, channel is missing — and no activeChannel filter is set, the message is still appended to the list with undefined fields, which can cause downstream rendering failures.

Consider adding an isValidMessagePayload guard consistent with the other stores:

function isValidMessagePayload(p: Record<string, unknown>): boolean {
  return (
    typeof p.id === 'string' && p.id !== '' &&
    typeof p.channel === 'string' &&
    typeof p.sender === 'string' &&
    typeof p.content === 'string' &&
    typeof p.timestamp === 'string'
  )
}

Then guard the handler body with if (!isValidMessagePayload(event.payload as Record<string, unknown>)) return.

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/stores/messages.ts
Line: 65-76

Comment:
**Missing structural validation on WS `message.sent` payload**

Every other store that handles WebSocket events has an explicit runtime type-guard before treating the payload as a domain type (`isValidAgentPayload`, `isValidApprovalPayload`, `isValidTaskPayload`, `isCostRecord`). This handler skips that step and directly casts `event.payload` to `Message`, then only guards on `message.id`.

If the payload is structurally malformed — for example, `channel` is missing — and no `activeChannel` filter is set, the message is still appended to the list with `undefined` fields, which can cause downstream rendering failures.

Consider adding an `isValidMessagePayload` guard consistent with the other stores:

```ts
function isValidMessagePayload(p: Record<string, unknown>): boolean {
  return (
    typeof p.id === 'string' && p.id !== '' &&
    typeof p.channel === 'string' &&
    typeof p.sender === 'string' &&
    typeof p.content === 'string' &&
    typeof p.timestamp === 'string'
  )
}
```

Then guard the handler body with `if (!isValidMessagePayload(event.payload as Record<string, unknown>)) return`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +53 to +90
} catch (err) {
error.value = getErrorMessage(err)
} finally {
loading.value = false
}
}

async function fetchAgentSpending(agentId: string): Promise<AgentSpending | null> {
loading.value = true
error.value = null
try {
return await budgetApi.getAgentSpending(agentId)
} catch (err) {
error.value = getErrorMessage(err)
return null
} finally {
loading.value = false
}
}

function handleWsEvent(event: WsEvent) {
if (event.event_type === 'budget.record_added') {
if (isCostRecord(event.payload)) {
// Skip if active filters don't match this record
if (lastFetchParams?.agent_id && event.payload.agent_id !== lastFetchParams.agent_id) return
if (lastFetchParams?.task_id && event.payload.task_id !== lastFetchParams.task_id) return
const limit = lastFetchParams?.limit ?? MAX_WS_RECORDS
records.value = [event.payload, ...records.value].slice(0, limit)
totalRecords.value++
}
}
}

return {
config,
records,
totalRecords,
loading,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shared loading flag across three independent fetch operations

fetchConfig, fetchRecords, and fetchAgentSpending all read/write the same loading ref. If two of them run concurrently:

  1. Both set loading = true
  2. The faster one finishes and sets loading = false in its finally block
  3. The UI loading indicator disappears while the slower request is still in flight

This matters particularly for fetchAgentSpending, which is called per-agent and is likely to overlap with a background fetchConfig or fetchRecords poll.

Consider using separate loading flags (e.g. configLoading, recordsLoading, spendingLoading) similar to the pattern already used in messages.ts (loading / channelsLoading) and company.ts (loading / departmentsLoading).

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/stores/budget.ts
Line: 53-90

Comment:
**Shared `loading` flag across three independent fetch operations**

`fetchConfig`, `fetchRecords`, and `fetchAgentSpending` all read/write the same `loading` ref. If two of them run concurrently:

1. Both set `loading = true`
2. The faster one finishes and sets `loading = false` in its `finally` block
3. The UI loading indicator disappears while the slower request is still in flight

This matters particularly for `fetchAgentSpending`, which is called per-agent and is likely to overlap with a background `fetchConfig` or `fetchRecords` poll.

Consider using separate loading flags (e.g. `configLoading`, `recordsLoading`, `spendingLoading`) similar to the pattern already used in `messages.ts` (`loading` / `channelsLoading`) and `company.ts` (`loading` / `departmentsLoading`).

How can I resolve this? If you propose a fix, please make it concise.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.82%. Comparing base (fa8bf1d) to head (be9cce7).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #344      +/-   ##
==========================================
+ Coverage   93.75%   93.82%   +0.07%     
==========================================
  Files         433      441       +8     
  Lines       19831    20243     +412     
  Branches     1911     1950      +39     
==========================================
+ Hits        18592    18993     +401     
- Misses        957      966       +9     
- Partials      282      284       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 21

♻️ Duplicate comments (7)
web/src/composables/useAuth.ts (1)

14-17: 🛠️ Refactor suggestion | 🟠 Major

Remove the widened string[] cast in canWrite (Line 16).

The cast drops role-union safety, so frontend/backend role drift becomes less compiler-visible.

♻️ Proposed fix
-  const canWrite = computed(() => {
-    const role = userRole.value
-    return role !== null && (WRITE_ROLES as ReadonlyArray<string>).includes(role)
-  })
+  const canWrite = computed(() => {
+    const role = userRole.value
+    return role !== null && WRITE_ROLES.includes(role)
+  })
--- a/web/src/utils/constants.ts
+++ b/web/src/utils/constants.ts
@@
+import type { HumanRole } from '@/api/types'
-export const WRITE_ROLES = ['ceo', 'manager', 'board_member', 'pair_programmer'] as const
+export const WRITE_ROLES: readonly HumanRole[] = [
+  'ceo',
+  'manager',
+  'board_member',
+  'pair_programmer',
+]
#!/bin/bash
set -euo pipefail

echo "1) Detect widened cast in useAuth:"
rg -n 'ReadonlyArray<string>\)\.includes\(role\)' web/src/composables/useAuth.ts

echo
echo "2) Inspect WRITE_ROLES declaration typing context:"
rg -n 'export const WRITE_ROLES' web/src/utils/constants.ts -C3
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/composables/useAuth.ts` around lines 14 - 17, The computed canWrite
currently casts WRITE_ROLES to ReadonlyArray<string>, widening the role type and
losing union safety; remove the cast in the canWrite computed (replace
(WRITE_ROLES as ReadonlyArray<string>).includes(role) with a direct
WRITE_ROLES.includes(role)) so TypeScript infers the original literal union from
WRITE_ROLES, and if the compiler still complains adjust WRITE_ROLES' declaration
(export const WRITE_ROLES = [...] as const) or narrow role typing (userRole's
type) rather than widening here; locate symbols canWrite, userRole, and
WRITE_ROLES to make this change.
web/src/stores/company.ts (1)

47-53: ⚠️ Potential issue | 🟠 Major

Pagination loop can skip records and may not terminate safely.

Using a fixed offset += MAX_PAGE_SIZE can skip data when page size differs from request size, and missing an empty-page guard risks an unbounded loop.

Proposed fix
       while (true) {
         const result = await companyApi.listDepartments({ limit: MAX_PAGE_SIZE, offset })
         if (gen !== departmentsGen) return // Stale request — abort
-        allDepts = [...allDepts, ...result.data]
-        if (allDepts.length >= result.total) break
-        offset += MAX_PAGE_SIZE
+        if (result.data.length === 0) break
+        allDepts.push(...result.data)
+        offset += result.data.length
+        if (allDepts.length >= result.total) break
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/company.ts` around lines 47 - 53, The pagination loop can skip
records and loop forever because it advances by MAX_PAGE_SIZE instead of the
actual number of items returned and lacks an empty-page guard; update the loop
that calls companyApi.listDepartments (and uses departmentsGen, allDepts,
offset, MAX_PAGE_SIZE) to advance offset by result.data.length, and add a guard
to break when result.data.length === 0 or when allDepts.length >= result.total;
keep the stale-request check (gen !== departmentsGen) and ensure you handle
zero-length pages to avoid an unbounded loop.
web/src/stores/approvals.ts (1)

16-23: ⚠️ Potential issue | 🟠 Major

activeFilters is recorded but not reapplied after local or WS mutations.

After a filtered fetch, approve()/reject() and the approved/rejected/expired WS paths only patch rows in place, so a status: 'pending' view can keep items that no longer match. approval.submitted also bumps total even when the new item should not appear in the active result set. The store needs to reapply the stored filters/pagination or refetch before mutating cached rows and counts.

Also applies to: 31-53, 66-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/approvals.ts` around lines 16 - 23, fetchApprovals records
activeFilters but the store mutators (approve, reject) and WS handlers (e.g.,
approval.submitted, approved/rejected/expired paths) patch rows and bump total
without reapplying filters/pagination, causing filtered views (like
status:'pending') to show stale or out-of-scope items; update the store so that
before mutating approvals.value or total.value you reapply activeFilters and
current pagination (or re-run approvalsApi.listApprovals using activeFilters) to
determine whether the affected item should remain, and only update or
insert/remove rows and adjust total when the filtered result dictates — change
logic in fetchApprovals, approve, reject and the WS handlers to consult
activeFilters (and fetch/refetch when needed) rather than blindly patching
cached rows.
web/src/stores/budget.ts (1)

9-22: ⚠️ Potential issue | 🟠 Major

call_category is still missing from the WS runtime guard.

CostRecord requires call_category, but isCostRecord() never checks it. A partial budget.record_added payload can still be accepted and stored as a full CostRecord.

Close the remaining validation gap
   return (
     typeof p.agent_id === 'string' &&
     typeof p.task_id === 'string' &&
     typeof p.provider === 'string' &&
     typeof p.model === 'string' &&
     typeof p.cost_usd === 'number' &&
     typeof p.input_tokens === 'number' &&
     typeof p.output_tokens === 'number' &&
-    typeof p.timestamp === 'string'
+    typeof p.timestamp === 'string' &&
+    (p.call_category === null ||
+      p.call_category === 'productive' ||
+      p.call_category === 'coordination' ||
+      p.call_category === 'system')
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/budget.ts` around lines 9 - 22, isCostRecord currently omits
validating the required call_category field, allowing malformed
budget.record_added payloads to pass; update the isCostRecord(payload: unknown):
payload is CostRecord guard to also check that (payload as Record<string,
unknown>).call_category is a string (i.e. add typeof p.call_category ===
'string' to the returned conjunction) so the runtime type guard fully matches
the CostRecord shape.
web/src/stores/agents.ts (1)

38-74: ⚠️ Potential issue | 🟠 Major

The WS handler still stores partial agents as full AgentConfigs.

isValidAgentPayload() only proves four top-level strings, but Line 57 casts the payload to AgentConfig, which also requires level, status, personality, model, skills, memory, tools, and hiring_date. The raw spread on Line 73 can likewise overwrite an existing agent with malformed fields. Validate the full payload or refetch the canonical agent before committing it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/agents.ts` around lines 38 - 74, The WS handler
(handleWsEvent) currently treats partial payloads validated only by
isValidAgentPayload as full AgentConfig objects and casts/spreads them into
agents.value (e.g., on 'agent.hired' and 'agent.status_changed'), risking
malformed AgentConfig entries; fix by stopping direct casts of payload to
AgentConfig and either (a) fetch the canonical AgentConfig from the server when
the payload lacks full fields before adding/updating, or (b) strictly
whitelist-and-merge only safe top-level updatable fields (e.g., name, role,
department, status) into the existing agent instead of spreading the whole
payload; update the logic in agent.hired, agent.status_changed, and any code
that uses isValidAgentPayload to ensure only fully validated or refetched
AgentConfig objects are stored in agents.value.
web/src/views/LoginPage.vue (1)

18-67: ⚠️ Potential issue | 🟠 Major

Classify lockout failures from status/type, not from display strings.

The credential-failure check is coupled to three exact messages. Copy changes, 429/409 responses, or synthesized frontend errors from web/src/stores/auth.ts will still consume attempts, and the lockout text stored in error can remain after lockedUntil expires. This same block is duplicated in web/src/views/SetupPage.vue, so it should live in a shared composable that derives the lockout message from lockedUntil and classifies failures from the original error/status.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/views/LoginPage.vue` around lines 18 - 67, Replace fragile
string-based credential checks with a shared composable that classifies failures
from the original error/status and centralizes lockout state/message. Create a
composable (e.g., useLoginLockout) that exposes lockedUntil, attempts,
checkAndClearLockout, isCredentialFailure(errorOrResponse) and a derived
lockoutMessage (based on lockedUntil and LOGIN_LOCKOUT_MS); update handleLogin
to call isCredentialFailure(err) (inspect err.status, err.code or error.type
instead of display strings) and use the composable's lockoutMessage for
error.value when locked; extract the same logic from SetupPage.vue to reuse the
composable. Ensure symbols to update: handleLogin, checkAndClearLockout,
lockedUntil, attempts, LOGIN_MAX_ATTEMPTS, LOGIN_LOCKOUT_MS, and
getErrorMessage.
web/src/utils/errors.ts (1)

55-63: ⚠️ Potential issue | 🟠 Major

Do not treat plain Error messages as UI-safe.

This branch still passes through any short Error.message, so an upstream new Error(body.error) or other unknown throw site can leak backend details verbatim. Only an explicit frontend-safe error type should bypass sanitization; plain Errors should fall back to the generic copy.

Harden the trusted path
+export class FrontendError extends Error {}
+
 export function getErrorMessage(error: unknown): string {
@@
-  if (error instanceof Error) {
+  if (error instanceof FrontendError) {
     // Only surface messages from errors explicitly thrown by our own code.
     // Errors from unknown sources could contain backend internals.
     const msg = error.message
     if (msg && msg.length < 200 && !/^\{/.test(msg)) {
       return msg
     }
-    return 'An unexpected error occurred.'
   }
+  if (error instanceof Error) {
+    return 'An unexpected error occurred.'
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/errors.ts` around lines 55 - 63, The current branch in
errors.ts returns short Error.message text, which can leak backend details;
change the logic so only an explicit frontend-safe error class is allowed to
bypass sanitization: replace the "error instanceof Error" passthrough with a
check for a specific trusted type (e.g., FrontendSafeError / UIError) and if the
error is not that trusted type always return the generic 'An unexpected error
occurred.' message; if such a trusted class doesn't exist yet, add one (e.g.,
class FrontendSafeError extends Error) and use instanceof FrontendSafeError in
the branch that returns msg.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/user_guide.md`:
- Line 20: The phrase "Template configuration through the UI is planned for a
future release" is ambiguous relative to dashboard template selection; update
the sentence to explicitly state that template selection is already available
via the dashboard while custom template editing/configuration via the UI is
planned for a future release (e.g., change to "Templates can be selected via the
dashboard; custom template editing/configuration in the UI is planned for a
future release") so readers understand the difference between selection and
editable configuration.

In `@web/eslint.config.js`:
- Around line 1-3: Import the TypeScript ESLint plugin (e.g., import tsEslint
from '@typescript-eslint/eslint-plugin') alongside the existing imports
(pluginVue, pluginSecurity, tsParser) and add a config entry that targets
TypeScript and Vue files: include a files glob ['**/*.ts','**/*.vue'], register
the plugin under the '@typescript-eslint' key, and merge in the recommended
rules via tsEslint.configs.recommended.rules so TypeScript-specific rules like
`@typescript-eslint/no-explicit-any` and `@typescript-eslint/no-unused-vars` are
enforced.
- Around line 33-35: Move the top-level "ignores" entry to the beginning of the
ESLint flat config for visibility and change the pattern from 'dist/' to the
explicit glob 'dist/**'; specifically, update the top-level ignores property
(ignores: ['dist/**']) and place it before other config entries so the global
ignore is clearly visible.

In `@web/src/__tests__/router/guards.test.ts`:
- Around line 4-5: Tests import authGuard too early so module-level
persisted-auth restore in web/src/stores/auth.ts doesn't run after you seed
localStorage; call vi.resetModules(), set the auth_token in localStorage, then
re-import the guard module (e.g. const { authGuard } = await
import('@/router/guards')) before calling createPinia()/useAuthStore in the
authenticated test cases so the persisted-auth branch is exercised; apply the
same pattern for both affected blocks around the seeds at the test cases that
set auth_token (the sections currently using createPinia() on Lines ~66 and
~89).

In `@web/src/__tests__/stores/agents.test.ts`:
- Around line 13-48: Tests seed the same mutable mockAgent into store.agents and
handleWsEvent('agent.status_changed') mutates it, causing order-dependent
failures; fix by cloning mockAgent before assigning into store.agents in each
test (e.g., use structuredClone(mockAgent) or a deep-copy helper like
JSON.parse(JSON.stringify(mockAgent))) so each test gets a fresh instance, and
update the tests that call handleWsEvent, store.agents, or expect agent mutation
to use the cloned object.

In `@web/src/__tests__/stores/auth.test.ts`:
- Line 3: The tests import useAuthStore too early so the auth module restores
auth_token at module load before tests seed localStorage; update the
persisted-auth test cases to call vi.resetModules() (or equivalent) and seed
localStorage, then re-import the module with await import('@/stores/auth') and
then get the store via the re-imported useAuthStore to ensure the store reads
the persisted token (apply this pattern wherever persisted-auth cases are
present, e.g., cases testing restore/logout/fetchUser that currently import
useAuthStore at top-level).
- Around line 204-216: The console.error spy in this test is only restored after
assertions, which can leak the mocked console if an expectation fails; wrap the
test execution and assertions that call useAuthStore() and store.fetchUser()
(referencing mockGetMe, useAuthStore, and store.fetchUser) in a try/finally and
call consoleSpy.mockRestore() inside the finally so the spy is always restored
even when expectations throw.

In `@web/src/__tests__/utils/constants.test.ts`:
- Around line 14-23: Add an assertion to the TERMINAL_STATUSES test that checks
its size so accidental additions are caught: in the
describe('TERMINAL_STATUSES') block (where TERMINAL_STATUSES is referenced) add
an expectation like expect(TERMINAL_STATUSES.size).toBe(<expectedNumber>)
alongside the existing membership checks to ensure the set contains exactly the
intended entries.

In `@web/src/__tests__/utils/errors.test.ts`:
- Around line 15-112: The test suite for getErrorMessage is missing coverage for
404 and 503 HTTP statuses; add two unit tests in
web/src/__tests__/utils/errors.test.ts that construct axios-like error objects
with isAxiosError: true and response.status set to 404 and 503 respectively,
then assert getErrorMessage(axiosError) returns 'The requested resource was not
found.' for 404 and 'Service temporarily unavailable. Please try again later.'
for 503, matching the existing pattern used by other status-based tests.

In `@web/src/api/endpoints/approvals.ts`:
- Around line 22-34: The three POST calls in createApproval, approveApproval,
and rejectApproval are untyped so `response` becomes `any` and breaks the
`unwrap()` contract; update each call to pass the response generic (e.g.
apiClient.post<ApiResponse<ApprovalItem>>(...)) so the returned type is
AxiosResponse<ApiResponse<ApprovalItem>> before calling `unwrap()`, leaving the
URL, payload and existing encodeURIComponent(id) usage unchanged.

In `@web/src/api/endpoints/tasks.ts`:
- Around line 23-40: The task mutation functions createTask, updateTask,
transitionTask, and cancelTask call apiClient.post/patch without explicit
generics, causing unwrap<T>() to lose type information; update each call to pass
the ApiResponse<Task> generic to apiClient (e.g.,
apiClient.post<ApiResponse<Task>>(...) or
apiClient.patch<ApiResponse<Task>>(...)) so the returned AxiosResponse matches
unwrap's expected AxiosResponse<ApiResponse<T>> and preserves compile-time type
safety for Task.

In `@web/src/components/layout/Topbar.vue`:
- Around line 20-22: The Topbar.vue menu currently always includes the Settings
entry (label/icon/command with router.push('/settings')) but that route/view
isn't present yet; fix by gating creation of that menu item so it's only added
when the route actually exists—e.g. in the Topbar setup, replace the
unconditional object with a conditional push that checks the router
(router.getRoutes().some(r => r.path === '/settings') or
router.hasRoute('settings') if you use named routes) and only then add the {
label: 'Settings', icon: 'pi pi-cog', command: () => router.push('/settings') }
item.

In `@web/src/main.ts`:
- Around line 20-30: Extract the sanitizeForLog function into a shared utility
module (e.g., create and export it from a new file like utils/logging.ts or
utils/errors.ts) so other parts of the app can reuse and test it; move the
existing implementation of sanitizeForLog (including the same signature
sanitizeForLog(value: unknown, maxLen = 500): string) into that module, export
it, then replace the local function in web/src/main.ts with an import of
sanitizeForLog from the new module and update any other files that duplicate the
logic to import the shared function instead.

In `@web/src/stores/approvals.ts`:
- Around line 55-77: The payload check is too narrow: isValidApprovalPayload
only validates five fields so handleWsEvent can insert a partial object into
approvals.value and break consumers; update handleWsEvent's 'approval.submitted'
branch to either (a) validate all required ApprovalItem properties before
casting OR (b) fetch the full approval by id and insert the returned
ApprovalItem — e.g., call your existing API helper (e.g., fetch/getApprovalById)
using payload.id, await the full ApprovalItem, then insert that into
approvals.value (instead of casting payload), and only fall back to inserting a
payload if you can populate safe defaults for every required ApprovalItem field;
keep references to isValidApprovalPayload, handleWsEvent, and approvals.value
when making the change.

In `@web/src/stores/auth.ts`:
- Around line 83-100: performAuthFlow currently calls clearAuth on any
fetchUser() exception which wipes a valid new token on transient network/5xx
errors; update the catch in performAuthFlow so it only clears auth when
fetchUser indicates an authentication failure (e.g., examine the thrown error or
response status for 401/invalid-token) and otherwise rethrow or surface an error
without calling clearAuth; keep the existing separate check that clears auth
when user.value is falsy after fetchUser, and apply the same conditional-clear
logic to the other fetchUser call in the same function so only real auth
failures (not transient errors) remove the session.

In `@web/src/stores/messages.ts`:
- Around line 59-65: The handleWsEvent function currently casts event.payload to
Message and only checks message.id before appending; update it to validate
required fields (at minimum id, channel, sender) and expected shapes
(attachments array, metadata object, etc.) before pushing to messages.value.
Implement a small type guard or runtime checks around the const message =
event.payload... line to ensure message.channel matches activeChannel.value only
after validation, and if validation fails log or ignore the payload instead of
appending (still enforce MAX_WS_MESSAGES when appending). Use the names
handleWsEvent, message, activeChannel, messages.value and MAX_WS_MESSAGES to
locate where to add the checks.
- Around line 32-43: fetchMessages(channel) doesn't update the live WS filter
used by handleWsEvent (activeChannel), so fetching a different channel can leave
WebSocket events attributed to the wrong list; fix by syncing the channel state:
either set activeChannel = channel (or null when no channel) inside
fetchMessages before/after the request, or introduce and update a dedicated
lastFetchedChannel variable in fetchMessages and switch handleWsEvent to use
lastFetchedChannel instead of activeChannel; update both the initial
fetchMessages implementation and the similar code block around the other fetch
to keep WS filtering consistent.

In `@web/src/stores/tasks.ts`:
- Around line 114-137: The payload validation in isValidTaskPayload is too weak
before casting to Task in handleWsEvent — extend isValidTaskPayload to check
required Task fields your UI reads (e.g., type, priority, project, created_by,
timestamps) or alternatively treat the WS payload as partial: avoid casting
payload as Task in handleWsEvent and instead enqueue a REST fetch/hydration for
the new task (e.g., by calling your existing fetchTaskById or a refreshTasks
helper) so downstream code never relies on missing fields; update references to
isValidTaskPayload and the task.created handling accordingly.

In `@web/src/styles/global.css`:
- Around line 36-40: The universal selector "*" is applying scrollbar-width and
scrollbar-color globally which can hurt performance; scope these properties
instead by moving scrollbar-width and scrollbar-color off of "*" and into a more
targeted selector such as "html, body" or a dedicated scrollable container class
(e.g., ".scrollable") so only the main page or explicit scrollable elements
receive the styles; update the rules that reference scrollbar-width and
scrollbar-color accordingly and remove or narrow the global "*" rule to avoid
styling every element.

In `@web/src/utils/format.ts`:
- Around line 41-55: The locale is hardcoded in formatCurrency and formatNumber;
extract the locale into a configurable value (e.g., a LOCALE constant or app
config) and update both functions to use that value (or accept an optional
locale parameter) so formatting uses the configured locale instead of 'en-US';
update references to formatCurrency and formatNumber to pass a locale when
needed or rely on the shared configuration.

In `@web/vitest.config.ts`:
- Around line 12-19: Add an explicit test include pattern to the Vitest
configuration by setting test.include in the exported config (the test block in
vitest.config.ts) so tests are discovered predictably; update the test object
(where globals, environment, and coverage are defined) to include a clear glob
like a project-specific pattern (e.g., targeting files under src or tests) to
override defaults and prevent accidental discovery.

---

Duplicate comments:
In `@web/src/composables/useAuth.ts`:
- Around line 14-17: The computed canWrite currently casts WRITE_ROLES to
ReadonlyArray<string>, widening the role type and losing union safety; remove
the cast in the canWrite computed (replace (WRITE_ROLES as
ReadonlyArray<string>).includes(role) with a direct WRITE_ROLES.includes(role))
so TypeScript infers the original literal union from WRITE_ROLES, and if the
compiler still complains adjust WRITE_ROLES' declaration (export const
WRITE_ROLES = [...] as const) or narrow role typing (userRole's type) rather
than widening here; locate symbols canWrite, userRole, and WRITE_ROLES to make
this change.

In `@web/src/stores/agents.ts`:
- Around line 38-74: The WS handler (handleWsEvent) currently treats partial
payloads validated only by isValidAgentPayload as full AgentConfig objects and
casts/spreads them into agents.value (e.g., on 'agent.hired' and
'agent.status_changed'), risking malformed AgentConfig entries; fix by stopping
direct casts of payload to AgentConfig and either (a) fetch the canonical
AgentConfig from the server when the payload lacks full fields before
adding/updating, or (b) strictly whitelist-and-merge only safe top-level
updatable fields (e.g., name, role, department, status) into the existing agent
instead of spreading the whole payload; update the logic in agent.hired,
agent.status_changed, and any code that uses isValidAgentPayload to ensure only
fully validated or refetched AgentConfig objects are stored in agents.value.

In `@web/src/stores/approvals.ts`:
- Around line 16-23: fetchApprovals records activeFilters but the store mutators
(approve, reject) and WS handlers (e.g., approval.submitted,
approved/rejected/expired paths) patch rows and bump total without reapplying
filters/pagination, causing filtered views (like status:'pending') to show stale
or out-of-scope items; update the store so that before mutating approvals.value
or total.value you reapply activeFilters and current pagination (or re-run
approvalsApi.listApprovals using activeFilters) to determine whether the
affected item should remain, and only update or insert/remove rows and adjust
total when the filtered result dictates — change logic in fetchApprovals,
approve, reject and the WS handlers to consult activeFilters (and fetch/refetch
when needed) rather than blindly patching cached rows.

In `@web/src/stores/budget.ts`:
- Around line 9-22: isCostRecord currently omits validating the required
call_category field, allowing malformed budget.record_added payloads to pass;
update the isCostRecord(payload: unknown): payload is CostRecord guard to also
check that (payload as Record<string, unknown>).call_category is a string (i.e.
add typeof p.call_category === 'string' to the returned conjunction) so the
runtime type guard fully matches the CostRecord shape.

In `@web/src/stores/company.ts`:
- Around line 47-53: The pagination loop can skip records and loop forever
because it advances by MAX_PAGE_SIZE instead of the actual number of items
returned and lacks an empty-page guard; update the loop that calls
companyApi.listDepartments (and uses departmentsGen, allDepts, offset,
MAX_PAGE_SIZE) to advance offset by result.data.length, and add a guard to break
when result.data.length === 0 or when allDepts.length >= result.total; keep the
stale-request check (gen !== departmentsGen) and ensure you handle zero-length
pages to avoid an unbounded loop.

In `@web/src/utils/errors.ts`:
- Around line 55-63: The current branch in errors.ts returns short Error.message
text, which can leak backend details; change the logic so only an explicit
frontend-safe error class is allowed to bypass sanitization: replace the "error
instanceof Error" passthrough with a check for a specific trusted type (e.g.,
FrontendSafeError / UIError) and if the error is not that trusted type always
return the generic 'An unexpected error occurred.' message; if such a trusted
class doesn't exist yet, add one (e.g., class FrontendSafeError extends Error)
and use instanceof FrontendSafeError in the branch that returns msg.

In `@web/src/views/LoginPage.vue`:
- Around line 18-67: Replace fragile string-based credential checks with a
shared composable that classifies failures from the original error/status and
centralizes lockout state/message. Create a composable (e.g., useLoginLockout)
that exposes lockedUntil, attempts, checkAndClearLockout,
isCredentialFailure(errorOrResponse) and a derived lockoutMessage (based on
lockedUntil and LOGIN_LOCKOUT_MS); update handleLogin to call
isCredentialFailure(err) (inspect err.status, err.code or error.type instead of
display strings) and use the composable's lockoutMessage for error.value when
locked; extract the same logic from SetupPage.vue to reuse the composable.
Ensure symbols to update: handleLogin, checkAndClearLockout, lockedUntil,
attempts, LOGIN_MAX_ATTEMPTS, LOGIN_LOCKOUT_MS, and getErrorMessage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 27b0aa25-9dee-46bb-b7fe-3bebcb528800

📥 Commits

Reviewing files that changed from the base of the PR and between 2f10d49 and be9cce7.

⛔ Files ignored due to path filters (2)
  • web/package-lock.json is excluded by !**/package-lock.json
  • web/public/favicon.svg is excluded by !**/*.svg
📒 Files selected for processing (86)
  • .dockerignore
  • .github/workflows/ci.yml
  • .gitignore
  • CLAUDE.md
  • README.md
  • docker/web/Dockerfile
  • docs/design/operations.md
  • docs/getting_started.md
  • docs/roadmap/index.md
  • docs/user_guide.md
  • web/.env.example
  • web/app.js
  • web/env.d.ts
  • web/eslint.config.js
  • web/index.html
  • web/nginx.conf
  • web/package.json
  • web/src/App.vue
  • web/src/__tests__/api/client.test.ts
  • web/src/__tests__/components/EmptyState.test.ts
  • web/src/__tests__/components/PageHeader.test.ts
  • web/src/__tests__/components/StatusBadge.test.ts
  • web/src/__tests__/composables/useOptimisticUpdate.test.ts
  • web/src/__tests__/composables/usePolling.test.ts
  • web/src/__tests__/router/guards.test.ts
  • web/src/__tests__/stores/agents.test.ts
  • web/src/__tests__/stores/approvals.test.ts
  • web/src/__tests__/stores/auth.test.ts
  • web/src/__tests__/stores/budget.test.ts
  • web/src/__tests__/stores/messages.test.ts
  • web/src/__tests__/stores/tasks.test.ts
  • web/src/__tests__/stores/websocket.test.ts
  • web/src/__tests__/utils/constants.test.ts
  • web/src/__tests__/utils/errors.test.ts
  • web/src/__tests__/utils/format.test.ts
  • web/src/api/client.ts
  • web/src/api/endpoints/agents.ts
  • web/src/api/endpoints/analytics.ts
  • web/src/api/endpoints/approvals.ts
  • web/src/api/endpoints/auth.ts
  • web/src/api/endpoints/budget.ts
  • web/src/api/endpoints/company.ts
  • web/src/api/endpoints/health.ts
  • web/src/api/endpoints/messages.ts
  • web/src/api/endpoints/providers.ts
  • web/src/api/endpoints/tasks.ts
  • web/src/api/types.ts
  • web/src/components/common/EmptyState.vue
  • web/src/components/common/ErrorBoundary.vue
  • web/src/components/common/LoadingSkeleton.vue
  • web/src/components/common/PageHeader.vue
  • web/src/components/common/StatusBadge.vue
  • web/src/components/layout/AppShell.vue
  • web/src/components/layout/ConnectionStatus.vue
  • web/src/components/layout/Sidebar.vue
  • web/src/components/layout/Topbar.vue
  • web/src/composables/useAuth.ts
  • web/src/composables/useOptimisticUpdate.ts
  • web/src/composables/usePolling.ts
  • web/src/main.ts
  • web/src/primevue-preset.ts
  • web/src/router/guards.ts
  • web/src/router/index.ts
  • web/src/stores/agents.ts
  • web/src/stores/analytics.ts
  • web/src/stores/approvals.ts
  • web/src/stores/auth.ts
  • web/src/stores/budget.ts
  • web/src/stores/company.ts
  • web/src/stores/messages.ts
  • web/src/stores/providers.ts
  • web/src/stores/tasks.ts
  • web/src/stores/websocket.ts
  • web/src/styles/global.css
  • web/src/styles/theme.ts
  • web/src/utils/constants.ts
  • web/src/utils/errors.ts
  • web/src/utils/format.ts
  • web/src/views/LoginPage.vue
  • web/src/views/PlaceholderHome.vue
  • web/src/views/SetupPage.vue
  • web/style.css
  • web/tsconfig.json
  • web/tsconfig.node.json
  • web/vite.config.ts
  • web/vitest.config.ts
💤 Files with no reviewable changes (2)
  • web/style.css
  • web/app.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Test (Python 3.14)
🧰 Additional context used
📓 Path-based instructions (3)
docs/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Docs source: docs/ (Markdown, built with Zensical). Design spec: docs/design/ (7 pages: index, agents, organization, communication, engine, memory, operations).

Files:

  • docs/roadmap/index.md
  • docs/user_guide.md
  • docs/design/operations.md
  • docs/getting_started.md
.github/workflows/*.yml

📄 CodeRabbit inference engine (CLAUDE.md)

CI: Jobs (lint + type-check + test in parallel) → ci-pass gate. Pages workflow exports OpenAPI schema, builds Astro landing + Zensical docs, merges, deploys to GitHub Pages on push to main.

Files:

  • .github/workflows/ci.yml
docker/**

📄 CodeRabbit inference engine (CLAUDE.md)

Docker: Backend 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web: nginxinc/nginx-unprivileged, SPA routing, API/WebSocket proxy to backend. All Docker files in docker/ directory.

Files:

  • docker/web/Dockerfile
🧠 Learnings (9)
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to docker/** : Docker: Backend 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web: `nginxinc/nginx-unprivileged`, SPA routing, API/WebSocket proxy to backend. All Docker files in `docker/` directory.

Applied to files:

  • .dockerignore
  • docs/roadmap/index.md
  • CLAUDE.md
  • .gitignore
  • docker/web/Dockerfile
  • docs/getting_started.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to .github/workflows/*.yml : CI: Jobs (lint + type-check + test in parallel) → ci-pass gate. Pages workflow exports OpenAPI schema, builds Astro landing + Zensical docs, merges, deploys to GitHub Pages on push to main.

Applied to files:

  • .github/workflows/ci.yml
  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to .github/workflows/docker.yml : Docker CI: `.github/workflows/docker.yml` builds backend + web images, pushes to GHCR, signs with cosign. Scans: Trivy (CRITICAL = hard fail, HIGH = warn-only) + Grype (critical cutoff). CVE triage via `.github/.trivyignore.yaml` and `.github/.grype.yaml`. Images only pushed after scans pass.

Applied to files:

  • .github/workflows/ci.yml
  • CLAUDE.md
  • docker/web/Dockerfile
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to docs/**/*.md : Docs source: `docs/` (Markdown, built with Zensical). Design spec: `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations).

Applied to files:

  • README.md
  • CLAUDE.md
  • .gitignore
  • docs/design/operations.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Fix everything valid when review agents find issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes)—never skip or defer

Applied to files:

  • web/src/stores/approvals.ts
  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to .github/workflows/pages-preview.yml : PR Preview workflow: Builds site on PRs, validates `pr_number` is positive integer, rejects closed/cross-repo PRs on dispatch, injects 'Development Preview' banner, deploys to Cloudflare Pages (`synthorg-pr-preview`), each PR gets unique preview URL at `pr-<number>.synthorg-pr-preview.pages.dev`

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to pyproject.toml : Dependencies: all versions pinned with `==` in `pyproject.toml`. Groups: `test` and `dev` (includes test). Install via `uv sync`.

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to src/**/*.py : Linting: `uv run ruff check src/ tests/` and format with `uv run ruff format src/ tests/`

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-13T06:54:28.702Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T06:54:28.702Z
Learning: Applies to src/**/*.py : Type-checking: `uv run mypy src/ tests/` (strict mode)

Applied to files:

  • CLAUDE.md
🧬 Code graph analysis (38)
web/src/main.ts (2)
web/src/router/index.ts (1)
  • router (34-34)
web/src/primevue-preset.ts (1)
  • primeVueOptions (4-12)
web/src/__tests__/composables/useOptimisticUpdate.test.ts (1)
web/src/composables/useOptimisticUpdate.ts (1)
  • useOptimisticUpdate (18-59)
web/src/stores/company.ts (3)
web/src/api/types.ts (2)
  • CompanyConfig (392-396)
  • Department (381-385)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/utils/constants.ts (1)
  • MAX_PAGE_SIZE (15-15)
web/src/__tests__/stores/budget.test.ts (2)
web/src/api/types.ts (4)
  • CostRecord (331-341)
  • BudgetConfig (356-363)
  • AgentSpending (365-368)
  • WsEvent (519-524)
web/src/stores/budget.ts (1)
  • useBudgetStore (25-97)
web/src/stores/analytics.ts (2)
web/src/api/types.ts (1)
  • OverviewMetrics (372-377)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/stores/providers.ts (2)
web/src/api/types.ts (1)
  • ProviderConfig (414-418)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/composables/useOptimisticUpdate.ts (1)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/__tests__/stores/tasks.test.ts (2)
web/src/api/types.ts (2)
  • Task (152-178)
  • WsEvent (519-524)
web/src/stores/tasks.ts (1)
  • useTaskStore (15-167)
web/src/stores/budget.ts (2)
web/src/api/types.ts (4)
  • CostRecord (331-341)
  • BudgetConfig (356-363)
  • AgentSpending (365-368)
  • WsEvent (519-524)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/api/endpoints/analytics.ts (2)
web/src/api/types.ts (2)
  • OverviewMetrics (372-377)
  • ApiResponse (94-96)
web/src/api/client.ts (2)
  • apiClient (12-16)
  • unwrap (54-60)
web/src/__tests__/utils/format.test.ts (1)
web/src/utils/format.ts (6)
  • formatDate (6-17)
  • formatRelativeTime (22-36)
  • formatCurrency (41-48)
  • formatNumber (53-55)
  • formatUptime (60-70)
  • formatLabel (75-80)
web/src/api/endpoints/company.ts (2)
web/src/api/types.ts (5)
  • CompanyConfig (392-396)
  • ApiResponse (94-96)
  • PaginationParams (552-555)
  • Department (381-385)
  • PaginatedResponse (105-107)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrap (54-60)
  • unwrapPaginated (66-82)
web/src/stores/auth.ts (3)
web/src/api/types.ts (2)
  • UserInfoResponse (133-138)
  • HumanRole (46-51)
web/src/api/endpoints/auth.ts (3)
  • setup (11-14)
  • login (16-19)
  • changePassword (21-24)
web/src/utils/errors.ts (2)
  • isAxiosError (8-10)
  • getErrorMessage (16-66)
web/src/stores/agents.ts (3)
web/src/api/types.ts (2)
  • AgentConfig (313-327)
  • WsEvent (519-524)
web/src/utils/constants.ts (1)
  • MAX_PAGE_SIZE (15-15)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/__tests__/stores/websocket.test.ts (2)
web/src/stores/websocket.ts (1)
  • useWebSocketStore (30-245)
web/src/api/types.ts (1)
  • WsEvent (519-524)
web/src/api/endpoints/auth.ts (1)
web/src/api/client.ts (2)
  • apiClient (12-16)
  • unwrap (54-60)
web/src/api/endpoints/approvals.ts (2)
web/src/api/types.ts (1)
  • ApprovalFilters (257-263)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrapPaginated (66-82)
  • unwrap (54-60)
web/src/__tests__/router/guards.test.ts (2)
web/src/router/guards.ts (1)
  • authGuard (12-40)
web/src/stores/auth.ts (1)
  • useAuthStore (8-165)
web/src/api/endpoints/agents.ts (2)
web/src/api/types.ts (1)
  • PaginationParams (552-555)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrapPaginated (66-82)
  • unwrap (54-60)
web/src/api/endpoints/budget.ts (2)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrap (54-60)
  • unwrapPaginated (66-82)
web/src/api/types.ts (1)
  • PaginationParams (552-555)
web/src/__tests__/utils/errors.test.ts (1)
web/src/utils/errors.ts (2)
  • isAxiosError (8-10)
  • getErrorMessage (16-66)
web/src/stores/messages.ts (2)
web/src/api/types.ts (3)
  • Message (447-458)
  • Channel (462-466)
  • WsEvent (519-524)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/api/endpoints/health.ts (2)
web/src/api/types.ts (2)
  • HealthStatus (470-476)
  • ApiResponse (94-96)
web/src/api/client.ts (2)
  • apiClient (12-16)
  • unwrap (54-60)
web/src/__tests__/stores/agents.test.ts (1)
web/src/api/types.ts (2)
  • AgentConfig (313-327)
  • WsEvent (519-524)
web/src/__tests__/stores/approvals.test.ts (2)
web/src/api/types.ts (2)
  • ApprovalItem (221-236)
  • WsEvent (519-524)
web/src/stores/approvals.ts (1)
  • useApprovalStore (7-105)
web/src/api/endpoints/providers.ts (1)
web/src/api/client.ts (2)
  • apiClient (12-16)
  • unwrap (54-60)
web/src/__tests__/stores/messages.test.ts (1)
web/src/stores/messages.ts (1)
  • useMessageStore (9-87)
web/src/stores/approvals.ts (3)
web/src/api/types.ts (5)
  • ApprovalItem (221-236)
  • ApprovalFilters (257-263)
  • ApproveRequest (249-251)
  • RejectRequest (253-255)
  • WsEvent (519-524)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
src/ai_company/api/controllers/approvals.py (2)
  • approve (271-338)
  • reject (345-412)
web/src/stores/websocket.ts (2)
web/src/api/types.ts (3)
  • WsChannel (492-498)
  • WsEventHandler (548-548)
  • WsEvent (519-524)
web/src/utils/constants.ts (3)
  • WS_MAX_RECONNECT_ATTEMPTS (9-9)
  • WS_RECONNECT_BASE_DELAY (7-7)
  • WS_RECONNECT_MAX_DELAY (8-8)
web/src/composables/useAuth.ts (1)
web/src/utils/constants.ts (1)
  • WRITE_ROLES (52-52)
web/src/router/index.ts (1)
web/src/router/guards.ts (1)
  • authGuard (12-40)
web/src/__tests__/composables/usePolling.test.ts (1)
web/src/composables/usePolling.ts (1)
  • usePolling (9-56)
web/src/stores/tasks.ts (3)
web/src/api/types.ts (7)
  • Task (152-178)
  • TaskFilters (211-217)
  • CreateTaskRequest (180-190)
  • UpdateTaskRequest (192-199)
  • TransitionTaskRequest (201-205)
  • CancelTaskRequest (207-209)
  • WsEvent (519-524)
web/src/utils/errors.ts (1)
  • getErrorMessage (16-66)
web/src/api/endpoints/tasks.ts (4)
  • createTask (23-26)
  • updateTask (28-31)
  • transitionTask (33-36)
  • cancelTask (38-41)
web/src/api/endpoints/tasks.ts (2)
web/src/api/types.ts (7)
  • PaginatedResponse (105-107)
  • Task (152-178)
  • ApiResponse (94-96)
  • CreateTaskRequest (180-190)
  • UpdateTaskRequest (192-199)
  • TransitionTaskRequest (201-205)
  • CancelTaskRequest (207-209)
web/src/api/client.ts (3)
  • apiClient (12-16)
  • unwrapPaginated (66-82)
  • unwrap (54-60)
web/src/api/client.ts (1)
web/src/api/types.ts (2)
  • ApiResponse (94-96)
  • PaginatedResponse (105-107)
web/src/utils/constants.ts (1)
web/src/api/types.ts (1)
  • TaskStatus (5-14)
web/src/__tests__/utils/constants.test.ts (1)
web/src/utils/constants.ts (4)
  • TASK_STATUS_ORDER (23-33)
  • TERMINAL_STATUSES (36-36)
  • VALID_TRANSITIONS (39-49)
  • NAV_ITEMS (59-70)
web/src/__tests__/stores/auth.test.ts (1)
web/src/stores/auth.ts (1)
  • useAuthStore (8-165)
🪛 Biome (2.4.6)
web/src/styles/global.css

[error] 3-34: Tailwind-specific syntax is disabled.

(parse)

🪛 GitHub Check: CodeQL
web/src/stores/websocket.ts

[warning] 98-98: Log injection
Log entry depends on a user-provided value.


[warning] 106-106: Log injection
Log entry depends on a user-provided value.

🪛 GitHub Check: Dashboard Lint
web/src/stores/providers.ts

[warning] 16-16:
Generic Object Injection Sink

web/src/api/endpoints/providers.ts

[warning] 17-17:
Generic Object Injection Sink

web/src/stores/websocket.ts

[warning] 196-196:
Generic Object Injection Sink


[warning] 191-191:
Generic Object Injection Sink


[warning] 24-24:
Generic Object Injection Sink


[warning] 24-24:
Generic Object Injection Sink

🪛 LanguageTool
docs/user_guide.md

[style] ~21-~21: Using many exclamation marks might seem excessive (in this case: 3 exclamation marks for a text that’s 1272 characters long)
Context: ...he UI is planned for a future release. !!! info "Active Development" SynthOrg ...

(EN_EXCESSIVE_EXCLAMATION)

docs/design/operations.md

[grammar] ~969-~969: Please add a punctuation mark at the end of paragraph.
Context: ...# Web UI Features !!! info "In Progress" The Web UI is being built as a Vue...

(PUNCTUATION_PARAGRAPH_END)

🪛 markdownlint-cli2 (0.21.0)
docs/design/operations.md

[warning] 971-971: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

🪛 Stylelint (17.4.0)
web/src/styles/global.css

[error] 3-3: Unexpected unknown at-rule "@theme" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

The web dashboard is at [http://localhost:3000](http://localhost:3000).

Container configuration (ports, storage paths, log level) is defined in `docker/.env`. Organization setup and templates will be configurable through the dashboard once available.
Container configuration (ports, storage paths, log level) is defined in `docker/.env`. Organization setup is done via the dashboard. Template configuration through the UI is planned for a future release.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify “template configuration” to avoid ambiguity with dashboard template selection.

This sentence can be interpreted as templates not being configurable at all via dashboard, while the guide later says templates are selected there. Consider explicitly saying custom template editing is the future feature.

✏️ Suggested wording
-Container configuration (ports, storage paths, log level) is defined in `docker/.env`. Organization setup is done via the dashboard. Template configuration through the UI is planned for a future release.
+Container configuration (ports, storage paths, log level) is defined in `docker/.env`. Organization setup is done via the dashboard. Custom template configuration/editing through the UI is planned for a future release.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Container configuration (ports, storage paths, log level) is defined in `docker/.env`. Organization setup is done via the dashboard. Template configuration through the UI is planned for a future release.
Container configuration (ports, storage paths, log level) is defined in `docker/.env`. Organization setup is done via the dashboard. Custom template configuration/editing through the UI is planned for a future release.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/user_guide.md` at line 20, The phrase "Template configuration through
the UI is planned for a future release" is ambiguous relative to dashboard
template selection; update the sentence to explicitly state that template
selection is already available via the dashboard while custom template
editing/configuration via the UI is planned for a future release (e.g., change
to "Templates can be selected via the dashboard; custom template
editing/configuration in the UI is planned for a future release") so readers
understand the difference between selection and editable configuration.

Comment on lines +1 to +3
import pluginVue from 'eslint-plugin-vue'
import pluginSecurity from 'eslint-plugin-security'
import tsParser from '@typescript-eslint/parser'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding @typescript-eslint/eslint-plugin for TypeScript-specific rules.

The configuration imports only the TypeScript parser but not the plugin. This means TypeScript-specific lint rules (e.g., @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars) won't be enforced. For a TypeScript-first codebase, adding the plugin would catch more issues.

💡 Suggested addition
 import pluginVue from 'eslint-plugin-vue'
 import pluginSecurity from 'eslint-plugin-security'
 import tsParser from '@typescript-eslint/parser'
+import tseslint from '@typescript-eslint/eslint-plugin'

Then include the recommended rules in the config array:

{
  files: ['**/*.ts', '**/*.vue'],
  plugins: {
    '@typescript-eslint': tseslint,
  },
  rules: {
    ...tseslint.configs.recommended.rules,
  },
},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import pluginVue from 'eslint-plugin-vue'
import pluginSecurity from 'eslint-plugin-security'
import tsParser from '@typescript-eslint/parser'
import pluginVue from 'eslint-plugin-vue'
import pluginSecurity from 'eslint-plugin-security'
import tsParser from '@typescript-eslint/parser'
import tseslint from '@typescript-eslint/eslint-plugin'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/eslint.config.js` around lines 1 - 3, Import the TypeScript ESLint plugin
(e.g., import tsEslint from '@typescript-eslint/eslint-plugin') alongside the
existing imports (pluginVue, pluginSecurity, tsParser) and add a config entry
that targets TypeScript and Vue files: include a files glob
['**/*.ts','**/*.vue'], register the plugin under the '@typescript-eslint' key,
and merge in the recommended rules via tsEslint.configs.recommended.rules so
TypeScript-specific rules like `@typescript-eslint/no-explicit-any` and
`@typescript-eslint/no-unused-vars` are enforced.

Comment on lines +33 to +35
{
ignores: ['dist/'],
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Minor: Consider moving ignores to the start and using explicit glob pattern.

In ESLint flat config, global ignores are conventionally placed first for visibility. Additionally, dist/** is more explicit than dist/.

💅 Suggested adjustment
 export default [
+  {
+    ignores: ['dist/**'],
+  },
   ...pluginVue.configs['flat/essential'],
   pluginSecurity.configs.recommended,
   // ... rest of config
-  {
-    ignores: ['dist/'],
-  },
 ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/eslint.config.js` around lines 33 - 35, Move the top-level "ignores"
entry to the beginning of the ESLint flat config for visibility and change the
pattern from 'dist/' to the explicit glob 'dist/**'; specifically, update the
top-level ignores property (ignores: ['dist/**']) and place it before other
config entries so the global ignore is clearly visible.

Comment on lines +4 to +5
import { authGuard } from '@/router/guards'
import { useAuthStore } from '@/stores/auth'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reload the guard module after populating localStorage.

Line 4 imports authGuard before the authenticated tests write auth_token. Since web/src/stores/auth.ts restores persisted auth at module load, calling createPinia() again on Lines 66 and 89 does not rerun that logic, so these cases are not actually validating the persisted-auth branch. Use vi.resetModules() and re-import @/router/guards after seeding storage.

Also applies to: 62-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/router/guards.test.ts` around lines 4 - 5, Tests import
authGuard too early so module-level persisted-auth restore in
web/src/stores/auth.ts doesn't run after you seed localStorage; call
vi.resetModules(), set the auth_token in localStorage, then re-import the guard
module (e.g. const { authGuard } = await import('@/router/guards')) before
calling createPinia()/useAuthStore in the authenticated test cases so the
persisted-auth branch is exercised; apply the same pattern for both affected
blocks around the seeds at the test cases that set auth_token (the sections
currently using createPinia() on Lines ~66 and ~89).

Comment on lines +13 to +48
const mockAgent: AgentConfig = {
id: 'test-uuid-001',
name: 'alice',
role: 'Developer',
level: 'senior',
department: 'engineering',
status: 'active',
model: {
provider: 'test-provider',
model_id: 'example-large-001',
temperature: 0.7,
max_tokens: 4096,
fallback_model: null,
},
personality: {
traits: [],
communication_style: 'neutral',
risk_tolerance: 'medium',
creativity: 'high',
description: '',
openness: 0.5,
conscientiousness: 0.5,
extraversion: 0.5,
agreeableness: 0.5,
stress_response: 0.5,
decision_making: 'analytical',
collaboration: 'team',
verbosity: 'balanced',
conflict_approach: 'collaborate',
},
skills: { primary: ['python'], secondary: ['go'] },
memory: { type: 'session', retention_days: null },
tools: { access_level: 'standard', allowed: ['file_system', 'git'], denied: [] },
autonomy_level: null,
hiring_date: '2026-03-01',
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use fresh agent fixtures when seeding store state.

handleWsEvent('agent.status_changed') mutates the stored agent, and Lines 76, 91, and 104 insert the shared mockAgent object directly. Once one test changes status, later tests inherit that mutated fixture and become order-dependent. Clone mockAgent before assigning it to store.agents.

Also applies to: 76-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/stores/agents.test.ts` around lines 13 - 48, Tests seed the
same mutable mockAgent into store.agents and
handleWsEvent('agent.status_changed') mutates it, causing order-dependent
failures; fix by cloning mockAgent before assigning into store.agents in each
test (e.g., use structuredClone(mockAgent) or a deep-copy helper like
JSON.parse(JSON.stringify(mockAgent))) so each test gets a fresh instance, and
update the tests that call handleWsEvent, store.agents, or expect agent mutation
to use the cloned object.

Comment on lines +59 to +65
function handleWsEvent(event: WsEvent) {
if (event.event_type === 'message.sent') {
const message = event.payload as unknown as Message
if (message.id) {
// Only append and count if message matches active channel (or no filter is set)
if (!activeChannel.value || message.channel === activeChannel.value) {
messages.value = [...messages.value, message].slice(-MAX_WS_MESSAGES)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate message.sent payloads before appending them.

This currently trusts any WS payload as Message and only checks id. A partial event can still be appended with missing channel, sender, attachments, or metadata, which will break rendering and channel filtering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/messages.ts` around lines 59 - 65, The handleWsEvent function
currently casts event.payload to Message and only checks message.id before
appending; update it to validate required fields (at minimum id, channel,
sender) and expected shapes (attachments array, metadata object, etc.) before
pushing to messages.value. Implement a small type guard or runtime checks around
the const message = event.payload... line to ensure message.channel matches
activeChannel.value only after validation, and if validation fails log or ignore
the payload instead of appending (still enforce MAX_WS_MESSAGES when appending).
Use the names handleWsEvent, message, activeChannel, messages.value and
MAX_WS_MESSAGES to locate where to add the checks.

Comment on lines +114 to +137
/** Runtime check for minimum required Task fields on a WS payload. */
function isValidTaskPayload(p: Record<string, unknown>): boolean {
return (
typeof p.id === 'string' && p.id !== '' &&
typeof p.title === 'string' &&
typeof p.status === 'string'
)
}

function handleWsEvent(event: WsEvent) {
const payload = event.payload as Record<string, unknown> | null
if (!payload || typeof payload !== 'object') return
switch (event.event_type) {
case 'task.created':
if (
isValidTaskPayload(payload) &&
!tasks.value.some((t) => t.id === payload.id)
) {
// Only append if no active filters — filtered views are kept accurate by REST fetches
if (!hasActiveFilters()) {
tasks.value = [...tasks.value, payload as unknown as Task]
total.value++
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider stricter payload validation before casting to Task.

The isValidTaskPayload helper validates only id, title, and status, but the Task interface has additional required fields (e.g., type, priority, project, created_by). The cast on line 134 (payload as unknown as Task) bypasses type safety. If downstream code accesses these fields, it may encounter undefined at runtime.

If the WS server guarantees full Task payloads on task.created, the current approach is acceptable. Otherwise, consider either:

  1. Extending validation to cover fields the UI actually reads, or
  2. Treating WS-created tasks as partial and triggering a REST fetch to hydrate the full record.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/tasks.ts` around lines 114 - 137, The payload validation in
isValidTaskPayload is too weak before casting to Task in handleWsEvent — extend
isValidTaskPayload to check required Task fields your UI reads (e.g., type,
priority, project, created_by, timestamps) or alternatively treat the WS payload
as partial: avoid casting payload as Task in handleWsEvent and instead enqueue a
REST fetch/hydration for the new task (e.g., by calling your existing
fetchTaskById or a refreshTasks helper) so downstream code never relies on
missing fields; update references to isValidTaskPayload and the task.created
handling accordingly.

Comment on lines +36 to +40
/* Scrollbar styling — Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--color-surface-300) var(--color-surface-50);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Universal selector for scrollbar styling may have performance implications.

Applying scrollbar-width and scrollbar-color to * affects all elements. Consider scoping to specific scrollable containers or using :root / html, body if only the main page scroll needs styling.

💡 Alternative scoped approach
-/* Scrollbar styling — Firefox */
-* {
-  scrollbar-width: thin;
-  scrollbar-color: var(--color-surface-300) var(--color-surface-50);
-}
+/* Scrollbar styling — Firefox */
+html {
+  scrollbar-width: thin;
+  scrollbar-color: var(--color-surface-300) var(--color-surface-50);
+}
+
+.scrollable {
+  scrollbar-width: thin;
+  scrollbar-color: var(--color-surface-300) var(--color-surface-50);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/* Scrollbar styling — Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--color-surface-300) var(--color-surface-50);
}
/* Scrollbar styling — Firefox */
html {
scrollbar-width: thin;
scrollbar-color: var(--color-surface-300) var(--color-surface-50);
}
.scrollable {
scrollbar-width: thin;
scrollbar-color: var(--color-surface-300) var(--color-surface-50);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/styles/global.css` around lines 36 - 40, The universal selector "*"
is applying scrollbar-width and scrollbar-color globally which can hurt
performance; scope these properties instead by moving scrollbar-width and
scrollbar-color off of "*" and into a more targeted selector such as "html,
body" or a dedicated scrollable container class (e.g., ".scrollable") so only
the main page or explicit scrollable elements receive the styles; update the
rules that reference scrollbar-width and scrollbar-color accordingly and remove
or narrow the global "*" rule to avoid styling every element.

Comment on lines +41 to +55
export function formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 4,
}).format(value)
}

/**
* Format a number with locale-appropriate separators.
*/
export function formatNumber(value: number): string {
return new Intl.NumberFormat('en-US').format(value)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider locale configurability for internationalization.

formatCurrency and formatNumber hardcode 'en-US' locale. For an MVP this is acceptable, but consider extracting the locale to a constant or configuration if i18n support is planned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/format.ts` around lines 41 - 55, The locale is hardcoded in
formatCurrency and formatNumber; extract the locale into a configurable value
(e.g., a LOCALE constant or app config) and update both functions to use that
value (or accept an optional locale parameter) so formatting uses the configured
locale instead of 'en-US'; update references to formatCurrency and formatNumber
to pass a locale when needed or rely on the shared configuration.

Comment on lines +12 to +19
test: {
globals: true,
environment: 'jsdom',
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,vue}'],
exclude: ['src/**/*.d.ts', 'src/main.ts', 'src/__tests__/**'],
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding explicit test file include pattern.

The config omits a test.include pattern, so Vitest uses its default (**/*.{test,spec}.?(c|m)[jt]s?(x)). This works but explicit configuration improves clarity and prevents accidental test file discovery issues.

   test: {
     globals: true,
     environment: 'jsdom',
+    include: ['src/**/*.test.ts'],
     coverage: {
       provider: 'v8',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/vitest.config.ts` around lines 12 - 19, Add an explicit test include
pattern to the Vitest configuration by setting test.include in the exported
config (the test block in vitest.config.ts) so tests are discovered predictably;
update the test object (where globals, environment, and coverage are defined) to
include a clear glob like a project-specific pattern (e.g., targeting files
under src or tests) to override defaults and prevent accidental discovery.

@Aureliolo
Copy link
Copy Markdown
Owner Author

Closing in favor of a fresh PR after rebase on main.

@Aureliolo Aureliolo closed this Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants