feat: web dashboard core infrastructure (PR 1 of 2)#344
feat: web dashboard core infrastructure (PR 1 of 2)#344
Conversation
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
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds 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
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes ✨ Finishing Touches
🧪 Generate unit tests (beta)
✨ Simplify code
|
Dependency ReviewThe following issues were found:
|
Preview DeploymentURL: https://pr-344.synthorg-pr-preview.pages.dev Built from commit |
Summary of ChangesHello, 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
Changelog
Ignored Files
Activity
Using Gemini Code AssistThe 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
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 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
|
Greptile SummaryThis 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:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
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()
Prompt To Fix All With AIThis 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 |
| pendingSubscriptions = [] | ||
| // Re-subscribe to all active subscriptions (covers both reconnect and first-connect) | ||
| for (const sub of activeSubscriptions) { | ||
| try { |
There was a problem hiding this 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_passwill 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.There was a problem hiding this comment.
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.
| resolve: { | ||
| alias: { | ||
| '@': resolve(__dirname, 'src'), | ||
| }, |
| resolve: { | ||
| alias: { | ||
| '@': resolve(__dirname, 'src'), | ||
| }, | ||
| }, |
| /** Discriminated paginated response envelope. */ | ||
| export type PaginatedResponse<T> = | ||
| | { data: T[]; error: null; success: true; pagination: PaginationMeta } | ||
| | { data: null; error: string; success: false; pagination: null } | ||
|
|
web/src/api/endpoints/providers.ts
Outdated
| const result: Record<string, ProviderConfig> = {} | ||
| for (const [key, provider] of Object.entries(raw)) { | ||
| result[key] = stripSecrets(provider) | ||
| } | ||
| return result |
| function dispatchEvent(event: WsEvent) { | ||
| channelHandlers.get(event.channel)?.forEach((h) => { h(event) }) | ||
| channelHandlers.get('*')?.forEach((h) => { h(event) }) | ||
| } |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (2)
web/package-lock.jsonis excluded by!**/package-lock.jsonweb/public/favicon.svgis excluded by!**/*.svg
📒 Files selected for processing (86)
.dockerignore.github/workflows/ci.yml.gitignoreCLAUDE.mdREADME.mddocker/web/Dockerfiledocs/design/operations.mddocs/getting_started.mddocs/roadmap/index.mddocs/user_guide.mdweb/.env.exampleweb/app.jsweb/env.d.tsweb/eslint.config.jsweb/index.htmlweb/nginx.confweb/package.jsonweb/src/App.vueweb/src/__tests__/api/client.test.tsweb/src/__tests__/components/EmptyState.test.tsweb/src/__tests__/components/PageHeader.test.tsweb/src/__tests__/components/StatusBadge.test.tsweb/src/__tests__/composables/useOptimisticUpdate.test.tsweb/src/__tests__/composables/usePolling.test.tsweb/src/__tests__/router/guards.test.tsweb/src/__tests__/stores/agents.test.tsweb/src/__tests__/stores/approvals.test.tsweb/src/__tests__/stores/auth.test.tsweb/src/__tests__/stores/budget.test.tsweb/src/__tests__/stores/messages.test.tsweb/src/__tests__/stores/tasks.test.tsweb/src/__tests__/stores/websocket.test.tsweb/src/__tests__/utils/constants.test.tsweb/src/__tests__/utils/errors.test.tsweb/src/__tests__/utils/format.test.tsweb/src/api/client.tsweb/src/api/endpoints/agents.tsweb/src/api/endpoints/analytics.tsweb/src/api/endpoints/approvals.tsweb/src/api/endpoints/auth.tsweb/src/api/endpoints/budget.tsweb/src/api/endpoints/company.tsweb/src/api/endpoints/health.tsweb/src/api/endpoints/messages.tsweb/src/api/endpoints/providers.tsweb/src/api/endpoints/tasks.tsweb/src/api/types.tsweb/src/components/common/EmptyState.vueweb/src/components/common/ErrorBoundary.vueweb/src/components/common/LoadingSkeleton.vueweb/src/components/common/PageHeader.vueweb/src/components/common/StatusBadge.vueweb/src/components/layout/AppShell.vueweb/src/components/layout/ConnectionStatus.vueweb/src/components/layout/Sidebar.vueweb/src/components/layout/Topbar.vueweb/src/composables/useAuth.tsweb/src/composables/useOptimisticUpdate.tsweb/src/composables/usePolling.tsweb/src/main.tsweb/src/primevue-preset.tsweb/src/router/guards.tsweb/src/router/index.tsweb/src/stores/agents.tsweb/src/stores/analytics.tsweb/src/stores/approvals.tsweb/src/stores/auth.tsweb/src/stores/budget.tsweb/src/stores/company.tsweb/src/stores/messages.tsweb/src/stores/providers.tsweb/src/stores/tasks.tsweb/src/stores/websocket.tsweb/src/styles/global.cssweb/src/styles/theme.tsweb/src/utils/constants.tsweb/src/utils/errors.tsweb/src/utils/format.tsweb/src/views/LoginPage.vueweb/src/views/PlaceholderHome.vueweb/src/views/SetupPage.vueweb/style.cssweb/tsconfig.jsonweb/tsconfig.node.jsonweb/vite.config.tsweb/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.mddocs/roadmap/index.mddocs/design/operations.mddocs/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 indocker/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:
.gitignoreREADME.mdCLAUDE.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:
.gitignoredocs/roadmap/index.md.dockerignoredocs/getting_started.mddocker/web/DockerfileCLAUDE.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.ymlCLAUDE.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.ymldocker/web/DockerfileCLAUDE.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.) |
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and examine the nginx.conf file
find . -name "nginx.conf" -type fRepository: Aureliolo/synthorg
Length of output: 78
🏁 Script executed:
# Read the nginx.conf file to see the context
cat web/nginx.confRepository: Aureliolo/synthorg
Length of output: 2411
🏁 Script executed:
# Get line counts to ensure we understand the file structure
wc -l web/nginx.confRepository: 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.
| "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" |
There was a problem hiding this comment.
🧩 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.jsonRepository: 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.
web/src/__tests__/api/client.test.ts
Outdated
| 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') | ||
| } |
There was a problem hiding this comment.
🧩 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 -5Repository: Aureliolo/synthorg
Length of output: 100
🏁 Script executed:
# Check the structure of web/src
ls -la web/src/ 2>/dev/null | head -20Repository: 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.tsRepository: 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.tsRepository: Aureliolo/synthorg
Length of output: 1304
🏁 Script executed:
# Also check the client.ts interceptor setup
cat web/src/api/client.tsRepository: 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 -5Repository: Aureliolo/synthorg
Length of output: 44
🏁 Script executed:
# Check package.json to see Axios version
grep -A 2 -B 2 '"axios"' web/package.jsonRepository: 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.tsRepository: 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.
| 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).
| 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) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and examine the test file
fd -t f "EmptyState.test.ts" | head -5Repository: Aureliolo/synthorg
Length of output: 109
🏁 Script executed:
# Also find the EmptyState component
fd -t f "EmptyState.ts" -o "EmptyState.vue" | head -10Repository: Aureliolo/synthorg
Length of output: 209
🏁 Script executed:
# Search for EmptyState component file properly
fd -t f -i emptystateRepository: 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.tsRepository: 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.vueRepository: 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.
| 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 |
There was a problem hiding this comment.
🧩 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 -20Repository: Aureliolo/synthorg
Length of output: 4376
🏁 Script executed:
cat -n web/src/router/index.tsRepository: 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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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(' ') |
There was a problem hiding this comment.
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.
| // 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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).
web/src/views/PlaceholderHome.vue
Outdated
| <div class="flex items-center justify-center h-full text-slate-400"> | ||
| Dashboard loading… |
There was a problem hiding this comment.
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.
| <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.
There was a problem hiding this comment.
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.
| "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" | ||
| } |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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 '&' | ||
| case '<': return '<' | ||
| case '>': return '>' | ||
| case '"': return '"' | ||
| case '\'': return ''' | ||
| default: return c | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| /** Build a stable deduplication key for a subscription (sorted channels + sorted filter keys). */ |
| 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
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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), | ||
| '"' | ||
| ) | ||
| } | ||
| } | ||
| } |
| (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' | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this 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:
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.| // 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) |
There was a problem hiding this 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:
| // 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.| 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, |
There was a problem hiding this 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:
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.| } 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, |
There was a problem hiding this 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:
- Both set
loading = true - The faster one finishes and sets
loading = falsein itsfinallyblock - 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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 21
♻️ Duplicate comments (7)
web/src/composables/useAuth.ts (1)
14-17: 🛠️ Refactor suggestion | 🟠 MajorRemove the widened
string[]cast incanWrite(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 | 🟠 MajorPagination loop can skip records and may not terminate safely.
Using a fixed
offset += MAX_PAGE_SIZEcan 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
activeFiltersis 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 astatus: 'pending'view can keep items that no longer match.approval.submittedalso bumpstotaleven 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_categoryis still missing from the WS runtime guard.
CostRecordrequirescall_category, butisCostRecord()never checks it. A partialbudget.record_addedpayload can still be accepted and stored as a fullCostRecord.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 | 🟠 MajorThe WS handler still stores partial agents as full
AgentConfigs.
isValidAgentPayload()only proves four top-level strings, but Line 57 casts the payload toAgentConfig, which also requireslevel,status,personality,model,skills,memory,tools, andhiring_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 | 🟠 MajorClassify 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.tswill still consume attempts, and the lockout text stored inerrorcan remain afterlockedUntilexpires. This same block is duplicated inweb/src/views/SetupPage.vue, so it should live in a shared composable that derives the lockout message fromlockedUntiland 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 | 🟠 MajorDo not treat plain
Errormessages as UI-safe.This branch still passes through any short
Error.message, so an upstreamnew Error(body.error)or other unknown throw site can leak backend details verbatim. Only an explicit frontend-safe error type should bypass sanitization; plainErrors 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
⛔ Files ignored due to path filters (2)
web/package-lock.jsonis excluded by!**/package-lock.jsonweb/public/favicon.svgis excluded by!**/*.svg
📒 Files selected for processing (86)
.dockerignore.github/workflows/ci.yml.gitignoreCLAUDE.mdREADME.mddocker/web/Dockerfiledocs/design/operations.mddocs/getting_started.mddocs/roadmap/index.mddocs/user_guide.mdweb/.env.exampleweb/app.jsweb/env.d.tsweb/eslint.config.jsweb/index.htmlweb/nginx.confweb/package.jsonweb/src/App.vueweb/src/__tests__/api/client.test.tsweb/src/__tests__/components/EmptyState.test.tsweb/src/__tests__/components/PageHeader.test.tsweb/src/__tests__/components/StatusBadge.test.tsweb/src/__tests__/composables/useOptimisticUpdate.test.tsweb/src/__tests__/composables/usePolling.test.tsweb/src/__tests__/router/guards.test.tsweb/src/__tests__/stores/agents.test.tsweb/src/__tests__/stores/approvals.test.tsweb/src/__tests__/stores/auth.test.tsweb/src/__tests__/stores/budget.test.tsweb/src/__tests__/stores/messages.test.tsweb/src/__tests__/stores/tasks.test.tsweb/src/__tests__/stores/websocket.test.tsweb/src/__tests__/utils/constants.test.tsweb/src/__tests__/utils/errors.test.tsweb/src/__tests__/utils/format.test.tsweb/src/api/client.tsweb/src/api/endpoints/agents.tsweb/src/api/endpoints/analytics.tsweb/src/api/endpoints/approvals.tsweb/src/api/endpoints/auth.tsweb/src/api/endpoints/budget.tsweb/src/api/endpoints/company.tsweb/src/api/endpoints/health.tsweb/src/api/endpoints/messages.tsweb/src/api/endpoints/providers.tsweb/src/api/endpoints/tasks.tsweb/src/api/types.tsweb/src/components/common/EmptyState.vueweb/src/components/common/ErrorBoundary.vueweb/src/components/common/LoadingSkeleton.vueweb/src/components/common/PageHeader.vueweb/src/components/common/StatusBadge.vueweb/src/components/layout/AppShell.vueweb/src/components/layout/ConnectionStatus.vueweb/src/components/layout/Sidebar.vueweb/src/components/layout/Topbar.vueweb/src/composables/useAuth.tsweb/src/composables/useOptimisticUpdate.tsweb/src/composables/usePolling.tsweb/src/main.tsweb/src/primevue-preset.tsweb/src/router/guards.tsweb/src/router/index.tsweb/src/stores/agents.tsweb/src/stores/analytics.tsweb/src/stores/approvals.tsweb/src/stores/auth.tsweb/src/stores/budget.tsweb/src/stores/company.tsweb/src/stores/messages.tsweb/src/stores/providers.tsweb/src/stores/tasks.tsweb/src/stores/websocket.tsweb/src/styles/global.cssweb/src/styles/theme.tsweb/src/utils/constants.tsweb/src/utils/errors.tsweb/src/utils/format.tsweb/src/views/LoginPage.vueweb/src/views/PlaceholderHome.vueweb/src/views/SetupPage.vueweb/style.cssweb/tsconfig.jsonweb/tsconfig.node.jsonweb/vite.config.tsweb/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.mddocs/user_guide.mddocs/design/operations.mddocs/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 indocker/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:
.dockerignoredocs/roadmap/index.mdCLAUDE.md.gitignoredocker/web/Dockerfiledocs/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.ymlCLAUDE.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.ymlCLAUDE.mddocker/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.mdCLAUDE.md.gitignoredocs/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.tsCLAUDE.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. |
There was a problem hiding this comment.
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.
| 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.
| import pluginVue from 'eslint-plugin-vue' | ||
| import pluginSecurity from 'eslint-plugin-security' | ||
| import tsParser from '@typescript-eslint/parser' |
There was a problem hiding this comment.
🧹 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.
| 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.
| { | ||
| ignores: ['dist/'], | ||
| }, |
There was a problem hiding this comment.
🧹 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.
| import { authGuard } from '@/router/guards' | ||
| import { useAuthStore } from '@/stores/auth' |
There was a problem hiding this comment.
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).
| 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', | ||
| } |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| /** 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++ | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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:
- Extending validation to cover fields the UI actually reads, or
- 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.
| /* Scrollbar styling — Firefox */ | ||
| * { | ||
| scrollbar-width: thin; | ||
| scrollbar-color: var(--color-surface-300) var(--color-surface-50); | ||
| } |
There was a problem hiding this comment.
🧹 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.
| /* 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.
| 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) | ||
| } |
There was a problem hiding this comment.
🧹 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.
| test: { | ||
| globals: true, | ||
| environment: 'jsdom', | ||
| coverage: { | ||
| provider: 'v8', | ||
| include: ['src/**/*.{ts,vue}'], | ||
| exclude: ['src/**/*.d.ts', 'src/main.ts', 'src/__tests__/**'], | ||
| }, |
There was a problem hiding this comment.
🧹 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.
|
Closing in favor of a fresh PR after rebase on main. |
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).encodeURIComponenton all dynamic path segments; endpoint modules for auth, agents, tasks, budget, approvals, messages, analytics, company, providers, healthuseAuth(token lifecycle),usePolling(interval management),useOptimisticUpdate(rollback on failure)ci-passReview 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 passesnpm --prefix web run build— Vite production build succeedsnpm --prefix web run test— 165 tests passPart of #233 (page views in PR 2: #340)
🤖 Generated with Claude Code