feat: web dashboard with Vue 3 + PrimeVue + Tailwind CSS#347
feat: web dashboard with Vue 3 + PrimeVue + Tailwind CSS#347
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
- 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
…odeQL - Fix 401 interceptor to sync Pinia auth state via clearAuth (#3) - Guard fetchDepartments against infinite loop on empty pages (#4) - Add isValidMessagePayload runtime guard and sync activeChannel on fetch (#5) - Split budget store loading into per-operation flags (#6) - Clone mockAgent fixtures to prevent test order-dependence (#8) - Wrap console.error spy in finally block in auth tests (#10) - Add TERMINAL_STATUSES size assertion (#11) - Add 404/503 status code test coverage (#12) - Add generic type params to approval/task endpoint post/patch calls (#13,#14) - Extract sanitizeForLog to shared utils/logging module (#16) - Deepen approval and task WS payload validation (#17,#21) - Distinguish transient vs auth errors in performAuthFlow (#18) - Sync WS channel filter with fetchMessages channel param (#19) - Scope scrollbar CSS to html instead of universal selector (#22) - Clarify user_guide.md template configuration wording (#25) - Move ESLint ignores to config start with explicit glob (#27)
Dependency ReviewThe following issues were found:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #347 +/- ##
=======================================
Coverage 93.90% 93.90%
=======================================
Files 447 447
Lines 20803 20803
Branches 2010 2010
=======================================
Hits 19535 19535
Misses 981 981
Partials 287 287 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR implements a full Vue 3 web dashboard for the SynthOrg platform, replacing the previous static placeholder page with a modern SPA built on PrimeVue, Tailwind CSS, Pinia, and Vue Router. It establishes the core frontend infrastructure including API client, state management, real-time WebSocket support, authentication flows, and CI integration.
Changes:
- Complete frontend infrastructure: Axios API client with JWT interceptor, 11 Pinia stores (auth, agents, tasks, budget, messages, approvals, websocket, analytics, company, providers), Vue Router with auth guards, and reusable composables
- Docker multi-stage build (Node builder → nginx runtime), CI pipeline with lint/type-check/test/build/audit jobs, and nginx config updates for WebSocket proxy and HSTS
- Documentation updates across CLAUDE.md, README, getting_started, user_guide, roadmap, and operations design pages reflecting the new dashboard
Reviewed changes
Copilot reviewed 85 out of 89 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| web/package.json | Frontend dependencies and scripts |
| web/vite.config.ts, vitest.config.ts | Build and test configuration |
| web/tsconfig.json, tsconfig.node.json | TypeScript configuration |
| web/eslint.config.js | ESLint with Vue and security plugins |
| web/src/api/client.ts | Axios client with JWT interceptor and response envelope unwrapping |
| web/src/api/types.ts | TypeScript interfaces mirroring backend Pydantic models |
| web/src/api/endpoints/*.ts | Typed API endpoint modules for all backend resources |
| web/src/stores/*.ts | 10 Pinia stores covering auth, data, and real-time state |
| web/src/router/index.ts, guards.ts | Vue Router with auth guard and redirect preservation |
| web/src/views/*.vue | Login, Setup, and Placeholder pages |
| web/src/components/**/*.vue | Layout shell (sidebar, topbar) and common components |
| web/src/composables/*.ts | usePolling, useOptimisticUpdate, useAuth |
| web/src/utils/*.ts | Error handling, formatting, constants, logging |
| web/src/styles/*.css, *.ts | Global CSS and theme tokens |
| web/src/tests/**/*.ts | 172 Vitest unit tests |
| web/nginx.conf | WebSocket proxy, HSTS, CSP updates |
| docker/web/Dockerfile | Multi-stage build with Node builder |
| .github/workflows/ci.yml | 5 new dashboard CI jobs |
| CLAUDE.md, README.md, docs/*.md | Documentation updates |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // TODO(#346): Enforce mustChangePassword redirect when | ||
| // /change-password route and page are added in the page-views PR. | ||
| // Currently only surfaced as a toast in LoginPage.vue. |
web/src/views/LoginPage.vue
Outdated
| const isCredentialError = msg !== 'Network error. Please check your connection.' && | ||
| msg !== 'A server error occurred. Please try again later.' && | ||
| msg !== 'Service temporarily unavailable. Please try again later.' | ||
| if (isCredentialError) { | ||
| 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.` | ||
| return | ||
| } | ||
| } | ||
| error.value = msg | ||
| } | ||
| } | ||
|
|
Greptile SummaryThis PR introduces a complete Vue 3 + PrimeVue + Tailwind CSS web dashboard — including 11 Pinia stores, a typed Axios API layer, a WebSocket store with exponential-backoff reconnection, login/setup views, shared layout components, 172 Vitest tests, an nginx reverse-proxy config, and a multi-stage Docker build — integrating cleanly into the existing backend architecture. The implementation is thorough and well-structured. Three issues were found:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Browser
participant Router as Vue Router
participant LoginPage
participant AuthStore
participant ApiClient as Axios Client
participant Backend
participant WsStore as WebSocket Store
Browser->>Router: Navigate to protected route (unauthenticated)
Router->>Browser: redirect /login?redirect=/original-path
Browser->>LoginPage: render
LoginPage->>AuthStore: login(username, password)
AuthStore->>ApiClient: POST /api/v1/auth/login
ApiClient->>Backend: credentials
Backend-->>ApiClient: token + expires_in
AuthStore->>AuthStore: setToken + scheduleExpiry timer
AuthStore->>ApiClient: GET /api/v1/auth/me
Backend-->>AuthStore: UserInfoResponse
AuthStore-->>LoginPage: resolves
Note over LoginPage,Router: Bug - always pushes to '/' ignoring redirect param
LoginPage->>Router: push('/')
Router->>WsStore: connect via AppShell on mount
WsStore->>Backend: WebSocket upgrade (auth via query param, TODO-343)
Backend-->>WsStore: connection open
WsStore->>Backend: subscribe channels
loop Real-time events
Backend->>WsStore: WsEvent payload
WsStore->>WsStore: dispatchEvent to store handlers
end
loop Session expiry or 401
ApiClient->>AuthStore: 401 interceptor fires
AuthStore->>Router: clearAuth + push to login
end
Prompt To Fix All With AIThis is a comment left during a code review.
Path: web/src/views/LoginPage.vue
Line: 32-35
Comment:
**Post-login redirect ignores `route.query.redirect`**
After a successful login, the handler always pushes to `'/'` without reading the `redirect` query parameter that `authGuard` injects in `guards.ts`. This means any user who navigates directly to a protected route (e.g. `/tasks`), gets bounced to `/login?redirect=%2Ftasks`, and then successfully authenticates will land on the dashboard root instead of their intended destination.
`guards.ts` correctly sets this up:
```ts
const redirect = to.fullPath !== '/' ? to.fullPath : undefined
next({ path: '/login', query: redirect ? { redirect } : undefined })
```
But `LoginPage.vue` never reads it back:
```suggestion
try {
await auth.login(username.value, password.value)
reset()
if (auth.mustChangePassword) {
toast.add({ severity: 'warn', summary: 'Password change required', life: 5000 })
}
const redirect = route.query.redirect
router.push(typeof redirect === 'string' && redirect.startsWith('/') ? redirect : '/')
} catch (err) {
```
You'll also need to import `useRoute` and instantiate it at the top of the script block:
```ts
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
```
The `startsWith('/')` guard prevents an open-redirect via a crafted `?redirect=https://evil.com`.
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/auth.ts
Line: 101-115
Comment:
**Unreachable 401 branch inside `performAuthFlow`'s inner catch**
The inner `catch (fetchErr)` block checks for a `401` status from `fetchUser`, but `fetchUser` never re-throws on a 401 — it handles it by calling `clearAuth()` and returning silently:
```ts
// fetchUser (lines 142–155)
if (isAxiosError(err) && err.response?.status === 401) {
clearAuth()
// ← no throw, so performAuthFlow's catch never sees a 401
} else {
throw err // only non-401 errors propagate
}
```
This means the `if (isAxiosError(fetchErr) && fetchErr.response?.status === 401)` branch on line 105 is dead code. The `if (!user.value)` check that follows the try/catch correctly handles the silent-clearAuth case, so the logic is still sound — but the dead branch adds noise and could mislead future maintainers into thinking 401s surface as exceptions here.
Consider removing the dead branch and updating the comment:
```suggestion
} catch (fetchErr) {
// fetchUser only throws for non-401 errors; 401s are handled silently
// (clearAuth + no rethrow). If we get here it's a transient network/5xx
// error — clear auth since the app can't function without user data.
clearAuth()
throw new Error(`${flowName} succeeded but failed to load user profile. Please check your connection and try again.`)
}
```
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/websocket.ts
Line: 82-88
Comment:
**`WS_MAX_MESSAGE_SIZE` constant is imported but never enforced**
`WS_MAX_MESSAGE_SIZE` (4096 bytes) is declared in `constants.ts` and imported here, but the `onmessage` handler never checks the size of incoming frames before parsing them. A misbehaving or malicious backend could send arbitrarily large payloads, causing unbounded memory allocation inside `JSON.parse`.
Consider guarding before parsing:
```suggestion
socket.onmessage = (event: MessageEvent) => {
if (typeof event.data === 'string' && event.data.length > WS_MAX_MESSAGE_SIZE) {
console.error('WebSocket message exceeds size limit, dropping')
return
}
let data: unknown
try {
data = JSON.parse(event.data)
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 26c8f79 |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a complete Vue 3 web dashboard (Vite, TypeScript, PrimeVue, Tailwind) with API client, extensive Pinia stores, WebSocket layer, router guards, components, tests, Docker multi-stage build, and CI jobs; removes legacy static frontend artifacts and updates docs/configs accordingly. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser
participant Router as Router/Guards
participant Auth as Auth Store
participant Local as LocalStorage
participant API as API Client
participant Backend
Browser->>Router: Navigate to protected route
Router->>Auth: isAuthenticated?
Auth->>Local: read token
alt token valid
Auth-->>Router: allow
else no token / expired
Auth-->>Router: redirect /login (with redirect query)
end
Browser->>Auth: login(credentials)
Auth->>API: POST /auth/login
API->>Backend: request
Backend-->>API: token
API-->>Auth: token
Auth->>Local: store token & expiry
Auth->>API: GET /auth/me
API->>Backend: request user
Backend-->>API: user
API-->>Auth: user (set isAuthenticated)
Auth-->>Browser: navigate to intended route
sequenceDiagram
participant Component as Vue Component
participant WSStore as WebSocket Store
participant Socket as WebSocket
participant Backend
Component->>WSStore: connect(token)
WSStore->>Socket: new WebSocket(url?token=...)
Socket-->>WSStore: OPEN
WSStore->>WSStore: connected=true
Component->>WSStore: subscribe(['agents'], {status:'active'})
WSStore->>Socket: send subscribe (if open) else queue
Backend-->>Socket: event (agent.status_changed)
Socket-->>WSStore: message
WSStore->>Component: dispatch handler -> store updates -> UI reacts
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes ✨ Finishing Touches
🧪 Generate unit tests (beta)
✨ Simplify code
|
…e, 401 redirect - Fix approval total.value++ outside filter guard (count drift bug) - Extract useLoginLockout composable from Login/Setup pages (dedup + use HTTP status codes instead of string matching) - Preserve redirect URL in 401 fallback handler
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 delivers the foundational web dashboard for the application, providing a modern and interactive user interface for monitoring and managing synthetic organizations. It establishes a robust frontend framework with Vue 3, integrating essential features like authentication, real-time data updates, and comprehensive state management. This significantly advances the project's user-facing capabilities and sets the stage for further feature development. 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
|
| } | ||
| router.push('/') | ||
| } catch (err) { | ||
| const lockoutMsg = recordFailure(err) |
There was a problem hiding this comment.
Post-login redirect ignores route.query.redirect
After a successful login, the handler always pushes to '/' without reading the redirect query parameter that authGuard injects in guards.ts. This means any user who navigates directly to a protected route (e.g. /tasks), gets bounced to /login?redirect=%2Ftasks, and then successfully authenticates will land on the dashboard root instead of their intended destination.
guards.ts correctly sets this up:
const redirect = to.fullPath !== '/' ? to.fullPath : undefined
next({ path: '/login', query: redirect ? { redirect } : undefined })But LoginPage.vue never reads it back:
| } | |
| router.push('/') | |
| } catch (err) { | |
| const lockoutMsg = recordFailure(err) | |
| try { | |
| await auth.login(username.value, password.value) | |
| reset() | |
| if (auth.mustChangePassword) { | |
| toast.add({ severity: 'warn', summary: 'Password change required', life: 5000 }) | |
| } | |
| const redirect = route.query.redirect | |
| router.push(typeof redirect === 'string' && redirect.startsWith('/') ? redirect : '/') | |
| } catch (err) { |
You'll also need to import useRoute and instantiate it at the top of the script block:
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()The startsWith('/') guard prevents an open-redirect via a crafted ?redirect=https://evil.com.
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/views/LoginPage.vue
Line: 32-35
Comment:
**Post-login redirect ignores `route.query.redirect`**
After a successful login, the handler always pushes to `'/'` without reading the `redirect` query parameter that `authGuard` injects in `guards.ts`. This means any user who navigates directly to a protected route (e.g. `/tasks`), gets bounced to `/login?redirect=%2Ftasks`, and then successfully authenticates will land on the dashboard root instead of their intended destination.
`guards.ts` correctly sets this up:
```ts
const redirect = to.fullPath !== '/' ? to.fullPath : undefined
next({ path: '/login', query: redirect ? { redirect } : undefined })
```
But `LoginPage.vue` never reads it back:
```suggestion
try {
await auth.login(username.value, password.value)
reset()
if (auth.mustChangePassword) {
toast.add({ severity: 'warn', summary: 'Password change required', life: 5000 })
}
const redirect = route.query.redirect
router.push(typeof redirect === 'string' && redirect.startsWith('/') ? redirect : '/')
} catch (err) {
```
You'll also need to import `useRoute` and instantiate it at the top of the script block:
```ts
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
```
The `startsWith('/')` guard prevents an open-redirect via a crafted `?redirect=https://evil.com`.
How can I resolve this? If you propose a fix, please make it concise.| throw new Error(`${flowName} failed: invalid session. Please try again.`) | ||
| } | ||
| clearAuth() | ||
| throw new Error(`${flowName} succeeded but failed to load user profile. Please check your connection and try again.`) | ||
| } | ||
| // If fetchUser silently cleared auth (e.g. 401), the flow should not succeed | ||
| if (!user.value) { | ||
| clearAuth() | ||
| throw new Error(`${flowName} succeeded but failed to load user profile. Please try again.`) | ||
| } | ||
| return result | ||
| } finally { | ||
| loading.value = false | ||
| } | ||
| } |
There was a problem hiding this comment.
Unreachable 401 branch inside performAuthFlow's inner catch
The inner catch (fetchErr) block checks for a 401 status from fetchUser, but fetchUser never re-throws on a 401 — it handles it by calling clearAuth() and returning silently:
// fetchUser (lines 142–155)
if (isAxiosError(err) && err.response?.status === 401) {
clearAuth()
// ← no throw, so performAuthFlow's catch never sees a 401
} else {
throw err // only non-401 errors propagate
}This means the if (isAxiosError(fetchErr) && fetchErr.response?.status === 401) branch on line 105 is dead code. The if (!user.value) check that follows the try/catch correctly handles the silent-clearAuth case, so the logic is still sound — but the dead branch adds noise and could mislead future maintainers into thinking 401s surface as exceptions here.
Consider removing the dead branch and updating the comment:
| throw new Error(`${flowName} failed: invalid session. Please try again.`) | |
| } | |
| clearAuth() | |
| throw new Error(`${flowName} succeeded but failed to load user profile. Please check your connection and try again.`) | |
| } | |
| // If fetchUser silently cleared auth (e.g. 401), the flow should not succeed | |
| if (!user.value) { | |
| clearAuth() | |
| throw new Error(`${flowName} succeeded but failed to load user profile. Please try again.`) | |
| } | |
| return result | |
| } finally { | |
| loading.value = false | |
| } | |
| } | |
| } catch (fetchErr) { | |
| // fetchUser only throws for non-401 errors; 401s are handled silently | |
| // (clearAuth + no rethrow). If we get here it's a transient network/5xx | |
| // error — clear auth since the app can't function without user data. | |
| clearAuth() | |
| throw new Error(`${flowName} succeeded but failed to load user profile. Please check your connection and try again.`) | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/stores/auth.ts
Line: 101-115
Comment:
**Unreachable 401 branch inside `performAuthFlow`'s inner catch**
The inner `catch (fetchErr)` block checks for a `401` status from `fetchUser`, but `fetchUser` never re-throws on a 401 — it handles it by calling `clearAuth()` and returning silently:
```ts
// fetchUser (lines 142–155)
if (isAxiosError(err) && err.response?.status === 401) {
clearAuth()
// ← no throw, so performAuthFlow's catch never sees a 401
} else {
throw err // only non-401 errors propagate
}
```
This means the `if (isAxiosError(fetchErr) && fetchErr.response?.status === 401)` branch on line 105 is dead code. The `if (!user.value)` check that follows the try/catch correctly handles the silent-clearAuth case, so the logic is still sound — but the dead branch adds noise and could mislead future maintainers into thinking 401s surface as exceptions here.
Consider removing the dead branch and updating the comment:
```suggestion
} catch (fetchErr) {
// fetchUser only throws for non-401 errors; 401s are handled silently
// (clearAuth + no rethrow). If we get here it's a transient network/5xx
// error — clear auth since the app can't function without user data.
clearAuth()
throw new Error(`${flowName} succeeded but failed to load user profile. Please check your connection and try again.`)
}
```
How can I resolve this? If you propose a fix, please make it concise.| } | ||
| return | ||
| } | ||
|
|
||
| if (msg.error) { | ||
| console.error('WebSocket error:', sanitizeForLog(msg.error)) | ||
| return |
There was a problem hiding this comment.
WS_MAX_MESSAGE_SIZE constant is imported but never enforced
WS_MAX_MESSAGE_SIZE (4096 bytes) is declared in constants.ts and imported here, but the onmessage handler never checks the size of incoming frames before parsing them. A misbehaving or malicious backend could send arbitrarily large payloads, causing unbounded memory allocation inside JSON.parse.
Consider guarding before parsing:
| } | |
| return | |
| } | |
| if (msg.error) { | |
| console.error('WebSocket error:', sanitizeForLog(msg.error)) | |
| return | |
| socket.onmessage = (event: MessageEvent) => { | |
| if (typeof event.data === 'string' && event.data.length > WS_MAX_MESSAGE_SIZE) { | |
| console.error('WebSocket message exceeds size limit, dropping') | |
| return | |
| } | |
| let data: unknown | |
| try { | |
| data = JSON.parse(event.data) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/stores/websocket.ts
Line: 82-88
Comment:
**`WS_MAX_MESSAGE_SIZE` constant is imported but never enforced**
`WS_MAX_MESSAGE_SIZE` (4096 bytes) is declared in `constants.ts` and imported here, but the `onmessage` handler never checks the size of incoming frames before parsing them. A misbehaving or malicious backend could send arbitrarily large payloads, causing unbounded memory allocation inside `JSON.parse`.
Consider guarding before parsing:
```suggestion
socket.onmessage = (event: MessageEvent) => {
if (typeof event.data === 'string' && event.data.length > WS_MAX_MESSAGE_SIZE) {
console.error('WebSocket message exceeds size limit, dropping')
return
}
let data: unknown
try {
data = JSON.parse(event.data)
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive web dashboard using Vue 3, PrimeVue, and Tailwind CSS. The changes include the core infrastructure, Pinia stores, API layer, shared components, and extensive testing. The overall quality of the code is very high, with excellent attention to detail in areas like security, error handling, and state management. I've found one critical issue with dependency versions in package.json that will prevent the project from being set up, and one minor suggestion for .gitignore to improve clarity.
| "devDependencies": { | ||
| "@types/node": "^25.5.0", | ||
| "vite": "^6", | ||
| "@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.
Several devDependencies are pinned to versions that do not exist, which will cause npm install to fail. For example, vite is at ^6 while the latest is ^5.x.x, and vitest is at ^3 while the latest is ^1.x.x. Using pre-release versions for core dependencies like Vue and Tailwind is also a risk to stability.
I've suggested updating to the latest stable or compatible versions for the broken dependencies to ensure the project is buildable.
"devDependencies": {
"@types/node": "^20.14.9",
"vite": "^5.3.1",
"@typescript-eslint/parser": "^7.15.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.6.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.24"
}| *.tsbuildinfo | ||
| web/vite.config.d.ts | ||
| web/vite.config.js | ||
| web/vitest.config.d.ts | ||
| web/vitest.config.js |
There was a problem hiding this comment.
These lines ignore generated JavaScript and TypeScript declaration files for your Vite and Vitest configurations. While this is likely correct because you've written the configurations in TypeScript (.ts), it's an uncommon setup that might confuse new contributors. Consider adding a comment to clarify that these are build artifacts and are intentionally ignored.
🤖 I have created a release *beep* *boop* --- ## [0.1.3](v0.1.2...v0.1.3) (2026-03-13) ### Features * add Mem0 memory backend adapter ([#345](#345)) ([2788db8](2788db8)), closes [#206](#206) * centralized single-writer TaskEngine with full CRUD API ([#328](#328)) ([9c1a3e1](9c1a3e1)) * incremental AgentEngine → TaskEngine status sync ([#331](#331)) ([7a68d34](7a68d34)), closes [#323](#323) * web dashboard pages — views, components, tests, and review fixes ([#354](#354)) ([b165ec4](b165ec4)) * web dashboard with Vue 3 + PrimeVue + Tailwind CSS ([#347](#347)) ([06416b1](06416b1)) ### Bug Fixes * harden coordination pipeline with validators, logging, and fail-fast ([#333](#333)) ([2f10d49](2f10d49)), closes [#205](#205) * repo-wide security hardening from ZAP, Scorecard, and CodeQL audit ([#357](#357)) ([27eb288](27eb288)) ### CI/CD * add pip-audit, hadolint, OSSF Scorecard, ZAP DAST, and pre-push hooks ([#350](#350)) ([2802d20](2802d20)) * add workflow_dispatch trigger to PR Preview for Dependabot PRs ([#326](#326)) ([4c7b6d9](4c7b6d9)) * bump astral-sh/setup-uv from 7.4.0 to 7.5.0 in the minor-and-patch group ([#335](#335)) ([98dd8ca](98dd8ca)) ### Maintenance * bump the minor-and-patch group across 1 directory with 3 updates ([#352](#352)) ([031b1c9](031b1c9)) * **deps:** bump devalue from 5.6.3 to 5.6.4 in /site in the npm_and_yarn group across 1 directory ([#324](#324)) ([9a9c600](9a9c600)) * migrate docs build from MkDocs to Zensical ([#330](#330)) ([fa8bf1d](fa8bf1d)), closes [#329](#329) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
PR 1 of 2 — core infrastructure, stores, API layer, and shared components. A follow-up PR will add remaining page views and close #346.
sanitizeForLogutilityReview coverage
Test plan
npm --prefix web run lint— 0 errors (6 pre-existing warnings)npm --prefix web run type-check— cleannpm --prefix web run test— 172/172 tests passTowards #346 (requires follow-up page-views PR)
🤖 Generated with Claude Code