Skip to content

feat: all secondary dashboard views — vault, logs, tokens, analytics, graph, patterns, sessions, system#21

Merged
thebtf merged 25 commits into
mainfrom
feat/dashboard-secondary-views
Mar 19, 2026
Merged

feat: all secondary dashboard views — vault, logs, tokens, analytics, graph, patterns, sessions, system#21
thebtf merged 25 commits into
mainfrom
feat/dashboard-secondary-views

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Mar 19, 2026

Summary

All remaining dashboard views (Phases 6-9). Depends on: PR #20, #19, #18, #17

  • VaultView, TokensView, LogsView, AnalyticsView, GraphView, PatternsView, SessionsView, SystemView
  • Composables: useVault, useTokens, useLogs, usePatterns, useSessions
  • Components: TagEditor, CredentialRow, TokenRow, LogStream
  • Bulk operations + batch actions toolbar

Test plan

  • All 13 routes render without errors
  • Vault reveal shows value with 30s countdown
  • Token create shows value once
  • Log SSE stream with level filter
  • Graph renders with vis-network

Summary by CodeRabbit

  • Новые возможности

    • Полная поддержка API-токенов: создание, показ префикса, отзыв, подсчёт использования и массовая агрегация статистики.
    • Новые REST-эндпойнты: аутентификация через мастер-токен/куки, управление токенами, аналитика трендов, теги наблюдений, операции Vault, обслуживание и индексация сессий.
  • Обновление пользовательского интерфейса

    • Авторизация, панель, заголовок, страницы токенов/vault/аналитики/поиска/сессий/наблюдений и множество новых компонентов (SearchBar, Pagination, ConfirmDialog и др.).
    • Новые шрифты и маршрутизация.

thebtf added 23 commits March 19, 2026 02:07
Create api_tokens table for client token authentication with bcrypt
hash storage, prefix-based lookup index, and usage tracking columns.
GORM-based store with Create, List, FindByPrefix, Revoke,
IncrementStats, IncrementErrorCount, GetByID, and BatchIncrementStats
methods. APIToken model added to models.go.
Handlers for POST /api/auth/login (master token validation + HMAC cookie),
POST /api/auth/logout (cookie clear), GET /api/auth/me (status check),
GET/POST /api/auth/tokens (list/create), DELETE /api/auth/tokens/:id (revoke).
All handlers include swaggo annotations.
TokenAuth now supports three auth methods: master token (admin),
client API tokens (eng_* prefix with bcrypt verification and scope
enforcement), and HMAC-SHA256 signed session cookies (dashboard).
Read-only tokens are restricted to GET + whitelisted POST endpoints.
HMAC cookie key derived deterministically from master token via SHA-256.
Add public routes (login, logout) and authenticated routes (me, tokens
CRUD) to setupRoutes(). Create TokenStore in initializeAsync() and
wire it into TokenAuth middleware after DB initialization.
Background goroutine reads token IDs from a buffered channel and
flushes accumulated request counts to the database every 5 seconds,
reducing per-request UPDATE overhead for client token authentication.
New handlers in handlers_vault.go:
- GET /api/vault/credentials — list credentials (no values)
- GET /api/vault/credentials/{name} — get with decrypted value
- POST /api/vault/credentials — store credential
- DELETE /api/vault/credentials/{name} — delete credential
- GET /api/vault/status — vault encryption status

All handlers delegate to existing crypto.Vault and ObservationStore.
Includes swaggo annotations for OpenAPI docs.
New handlers in handlers_tags.go:
- POST /api/observations/{id}/tags — add/remove/set tags
- GET /api/observations/by-tag/{tag} — list observations by tag

Logic mirrors MCP tag_observation and get_observations_by_tag handlers.
Includes swaggo annotations for OpenAPI docs.
New handlers in handlers_sessions_rest.go:
- GET /api/sessions-index — list indexed sessions
- GET /api/sessions-index/search — full-text search transcripts

Uses /api/sessions-index path to avoid conflict with existing
/api/sessions live session management routes.
Includes swaggo annotations for OpenAPI docs.
New handlers in handlers_maintenance.go:
- POST /api/maintenance/consolidation — trigger consolidation cycle
- POST /api/maintenance/run — trigger full maintenance
- GET /api/maintenance/stats — maintenance statistics

Delegates to existing consolidationScheduler and maintenanceService.
Includes swaggo annotations for OpenAPI docs.
New handler in handlers_analytics.go:
- GET /api/analytics/trends — temporal trends (obs per period)

Supports daily/weekly/hourly grouping with project filter.
Logic mirrors MCP get_temporal_trends handler.
Includes swaggo annotations for OpenAPI docs.
Wire all new REST handlers into the requireReady + auth route group:
- /api/vault/* (5 endpoints)
- /api/observations/{id}/tags, /api/observations/by-tag/{tag}
- /api/sessions-index, /api/sessions-index/search
- /api/maintenance/* (3 endpoints)
- /api/analytics/trends
Add composables for observation CRUD (useObservation), generic pagination
(usePagination), and search state (useSearch). Add API functions for
observation detail, update, archive, feedback, and search endpoints.
Add SearchResult types and rejected[] field to Observation type.
Replace placeholder with paginated observation list using server-side
pagination (20 items/page). Add project dropdown, type filter pills,
and concept filter. Each row is clickable to navigate to detail view.
Replace placeholder with full observation detail view including:
- Read-only display of all fields (narrative, facts, concepts, files, metadata)
- Rejected alternatives section for decision observations
- Inline edit mode with ObservationEditor component (explicit save/discard)
- beforeunload warning for unsaved changes
- Archive with confirmation dialog
- Feedback buttons (thumbs up/down)
Add SearchBar with "/" hotkey focus and compact/expanded modes.
Add SearchResults displaying relevance percentage, type badges,
concept tags, and rejected alternatives for decisions.
Implement SearchView with project filter, decision mode toggle,
and URL query parameter support for header search integration.
Replace inline search form with reusable SearchBar component in
compact mode. Adds "/" hotkey focus and consistent styling across
header and dedicated search page.
- VaultView: credential list, reveal with 30s countdown, copy, delete
- TokensView: token list with create modal and revoke confirmation
- useVault/useTokens composables for state management
- Extended api.ts with all new API functions for phases 6-9
- LogsView: SSE log stream with level filters, search, pause/resume
- useLogs composable: SSE connection, 1000-entry buffer, level toggle
- TagEditor component: add/remove concept tags on observations
- ObservationsView: bulk selection with select-all, archive, delete
- Batch action toolbar with confirmation dialog
- Search performance stats cards (total, latency, cache hits, errors)
- Latency breakdown with CSS bar charts (vector vs filter)
- Retrieval stats, search misses table, recent queries list
- Search type distribution visualization
- GraphView: vis-network graph with type-colored nodes, click navigation
- PatternsView: pattern list with type badges, expandable insight panels,
  deprecate and delete actions with confirmation
- SessionsView: session list with project/date filters, transcript search
- SystemView: component health cards, vector metrics, graph stats,
  maintenance controls (consolidation, maintenance), update checker
- usePatterns/useSessions composables for state management
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 19, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Добавлены серверные и клиентские возможности аутентификации и управления API-токенами: миграция и модель для api_tokens, GORM TokenStore с операциями и счётчиками, middleware и HTTP-обработчики для входа/выхода и управления токенами; дополнительно — множество REST-эндпоинтов и UI: маршрутизация, страницы, компоненты и composables для токенов, вольта, сессий, поиска, логов и аналитики.

Changes

Cohort / File(s) Summary
Миграции и модели
internal/db/gorm/migrations.go, internal/db/gorm/models.go
Добавлена миграция 036_api_tokens и модель APIToken с индексами, уникальными ограничениями и полями для хранилища/ревокации/статистики.
TokenStore (DB)
internal/db/gorm/token_store.go
Новый GORM-backed TokenStore: Create, List, FindByPrefix, Revoke, GetByID, IncrementStats, IncrementErrorCount, BatchIncrementStats (транзакции).
Аутентификация и middleware
internal/worker/handlers_auth.go, internal/worker/middleware.go
Добавлены HTTP-обработчики для login/logout/me и CRUD токенов; middleware поддерживает master-token header, cookie HMAC-сессии и DB токены с bcrypt и read-only ограничениями; сбор статистики токенов через канал.
Инициализация сервиса и флаши статистики
internal/worker/service.go, internal/worker/token_stats.go
Service получает TokenStore, маршруты для auth и новых API, запускает фоновый flusher для агрегированной записи статистики токенов.
Handlers: аналитика, обслуживание, теги, сессии, вольт
internal/worker/handlers_analytics.go, internal/worker/handlers_maintenance.go, internal/worker/handlers_tags.go, internal/worker/handlers_sessions_rest.go, internal/worker/handlers_vault.go
Новые REST-обработчики: тренды/аналитика, триггеры обслуживания, тегирование наблюдений, индексированные сессии (list/search), и Vault CRUD (шифрование/дешифрование, status).
UI: маршрутизация, точки входа, зависимости
ui/package.json, ui/main.ts, ui/src/router/index.ts, ui/tailwind.config.js
Добавлены зависимости шрифтов и vue-router, инициализирован роутер и глобальные импорты шрифтов, обновлён Tailwind с новыми fontFamily и цветами.
UI: глобальная авторизация и routing
ui/src/App.vue, ui/src/composables/useAuth.ts
App переписан под auth-gated routing; useAuth добавлен для проверки/логина/логаута с cookie credentials.
UI: страницы и представления
ui/src/views/* (12 файлов: Home, Search, Tokens, Vault, Logs, Patterns, Sessions, Analytics, System, Graph, ObservationDetail, Observations)
Добавлены 12 страниц с полной логикой: просмотр/редактирование наблюдений, токены, хранилище, лог-вьюер, аналитика, система, граф, наблюдения с пагинацией и т.д.
UI: компоненты макета и общие
ui/src/components/layout/*, ui/src/components/search/*, ui/src/components/observation/*
Новые переиспользуемые компоненты: Header, Sidebar, ConfirmDialog, EmptyState, Pagination, SearchBar, SearchResults, ObservationEditor, TagEditor.
UI: composables и утилиты
ui/src/composables/*, ui/src/utils/api.ts, ui/src/types/*
Множество new composables (useSearch, usePagination, useTokens, useVault, useLogs, usePatterns, useSessions, useObservation), расширенный API-клиент (api.ts) и новые типы для поиска/обсерваций.
Мелкие изменения UI
ui/src/components/layout/*, ui/src/main.ts, ui/package.json
Импорты шрифтов, router usage, экспорты composables и обновлённые package deps.

Диаграмма последовательности

sequenceDiagram
    participant Client
    participant Browser
    participant AuthHandler as AuthHandler/Middleware
    participant TokenStore
    participant Database

    Client->>Browser: Открывает SPA
    Browser->>AuthHandler: GET /api/auth/me (cookie/без токена)
    AuthHandler-->>Browser: 401 / redirect на /login

    Client->>Browser: POST /api/auth/login {token}
    Browser->>AuthHandler: POST /api/auth/login
    AuthHandler->>AuthHandler: const-time сравнение master token
    AuthHandler-->>Browser: Set-Cookie (HMAC-signed session)

    Browser->>AuthHandler: Запрос защищённого ресурса (Authorization: Bearer eng_...)
    AuthHandler->>TokenStore: FindByPrefix(prefix)
    TokenStore->>Database: SELECT ... WHERE token_prefix=? AND revoked=false
    Database-->>TokenStore: APIToken rows
    TokenStore->>AuthHandler: Верный токен / scope
    AuthHandler->>AuthHandler: Асинхронно push tokenID -> statsCh
    AuthHandler-->>Browser: 200 + данные
Loading

Estimated code review effort

🎯 5 (Критический) | ⏱️ ~120 минут

Possibly related PRs

Стихотворение

🐰 Я — кролик, храню ключи, грызу кэш,

eng_ префикс блеснёт в ночной тиши,
Хеш и HMAC в такт стучат, как ножки,
Токены пляшут — сервис бодрит ушки,
Пусть миграция прыг — и всё летит ввысь!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementation of all secondary dashboard views (vault, logs, tokens, analytics, graph, patterns, sessions, system) across multiple features.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-secondary-views
📝 Coding Plan
  • Generate coding plan for human review comments

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.3)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

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

This pull request significantly expands the dashboard's functionality by introducing a suite of new views for managing various aspects of the system, from API tokens and vault credentials to analytics and system health. It also lays the groundwork for a more robust and interactive user interface with enhanced authentication mechanisms and reusable components, ensuring a more complete and user-friendly experience.

Highlights

  • New Dashboard Views: Introduced several new dashboard views including Vault, Logs, Tokens, Analytics, Graph, Patterns, Sessions, and System, completing phases 6-9 of the dashboard development.
  • API Token Management: Implemented a comprehensive API token management system, allowing for the creation, listing, and revocation of client API tokens with configurable scopes (read-write, read-only).
  • Enhanced Authentication: Updated the authentication middleware to support three methods: master token, client API tokens with prefix-based lookup and bcrypt hashing, and HMAC-signed session cookies for dashboard access.
  • Database Migrations and Models: Added a new database migration to create the api_tokens table and introduced a corresponding APIToken GORM model to persist API token data.
  • UI Infrastructure and Components: Refactored the frontend application to use Vue Router for navigation, introduced new layout components (AppHeader, AppSidebar), and added reusable UI components like ConfirmDialog, EmptyState, Pagination, ObservationEditor, TagEditor, SearchBar, and SearchResults.
  • Backend API Endpoints: Developed new backend API handlers for analytics trends, authentication (login, logout, token management), maintenance operations, indexed session listing and searching, observation tag management, and vault credential management.
  • Asynchronous Token Stats: Implemented a buffered token stats flusher to asynchronously update API token usage statistics in the database, reducing per-request overhead.
  • Observation Management Enhancements: Added capabilities for editing observation details (title, subtitle, narrative, scope, concepts), archiving observations, and providing user feedback directly from the UI.
Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

Footnotes

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (17)
internal/db/gorm/models.go-370-370 (1)

370-370: ⚠️ Potential issue | 🟡 Minor

Исправьте синтаксис значения по умолчанию для Scope в GORM.

Значение read-write содержит дефис и должно быть заключено в одинарные кавычки в GORM теге. Текущий синтаксис default:read-write без кавычек не соответствует определению в миграции базы данных, где используется DEFAULT 'read-write'.

Предлагаемое исправление
-	Scope        string     `gorm:"type:text;not null;default:read-write"`
+	Scope        string     `gorm:"type:text;not null;default:'read-write'"`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/db/gorm/models.go` at line 370, Поле структуры Scope использует
некорректный GORM default без кавычек; замените метку gorm для поля Scope (Scope
string) так, чтобы значение по умолчанию было в одинарных кавычках (например
default:'read-write'), чтобы соответствовать миграции и корректному синтаксису
GORM.
ui/src/components/layout/Pagination.vue-14-15 (1)

14-15: ⚠️ Potential issue | 🟡 Minor

Потенциальное деление на ноль при limit <= 0.

Если limit равен 0 или отрицательному значению, вычисления totalPages и currentPage приведут к Infinity или NaN. Рекомендуется добавить защитную проверку.

🛡️ Предлагаемое исправление
-const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.limit)))
-const currentPage = computed(() => Math.floor(props.offset / props.limit) + 1)
+const safeLimit = computed(() => Math.max(1, props.limit))
+const totalPages = computed(() => Math.max(1, Math.ceil(props.total / safeLimit.value)))
+const currentPage = computed(() => Math.floor(props.offset / safeLimit.value) + 1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/layout/Pagination.vue` around lines 14 - 15, Guard against
division-by-zero by validating props.limit before computing pages; update the
computed properties totalPages and currentPage to use a safeLimit (e.g., if
props.limit <= 0 fall back to 1) or return sensible defaults (totalPages = 1 and
currentPage = 1) when limit is non-positive, ensuring calculations use safeLimit
for Math.ceil(props.total / safeLimit) and Math.floor(props.offset / safeLimit)
+ 1 and preserving reactive computed usage.
ui/src/views/GraphView.vue-208-208 (1)

208-208: ⚠️ Potential issue | 🟡 Minor

Нереактивная проверка DOM в шаблоне.

Условие !graphContainer?.querySelector('canvas') не является реактивным — Vue не отслеживает изменения DOM. После рендеринга графа условие не пересчитается автоматически, и статистика может отображаться некорректно.

🔧 Предлагаемое исправление с использованием реактивного флага

В секции <script setup>:

 const observationIdInput = ref('')
 const projects = ref<string[]>([])
+const graphRendered = ref(false)

 let network: Network | null = null

В функции renderGraph:

   network.on('click', (params) => {
     if (params.nodes.length > 0) {
       const nodeId = params.nodes[0]
       router.push({ name: 'observation-detail', params: { id: nodeId } })
     }
   })
+  graphRendered.value = true
 }

В шаблоне:

-    <div v-if="graphStats && !graphContainer?.querySelector('canvas')" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
+    <div v-if="graphStats && !graphRendered" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/GraphView.vue` at line 208, The template uses a non-reactive DOM
check (!graphContainer?.querySelector('canvas')) so Vue won't update the
condition after the graph is rendered; introduce a reactive ref (e.g., hasCanvas
or canvasRendered) in the <script setup>, initialize it based on graphContainer
presence, update it to true at the end of the renderGraph function (and set
false before re-render if needed), and replace the template condition to use the
reactive ref alongside graphStats (e.g., v-if="graphStats && !hasCanvas") so the
UI updates correctly when the canvas is added or removed; reference
graphContainer, renderGraph and graphStats to locate where to add and update the
reactive flag.
ui/src/views/AnalyticsView.vue-31-47 (1)

31-47: ⚠️ Potential issue | 🟡 Minor

Внешний блок catch недостижим — ошибки перехватываются раньше.

Поскольку каждый вызов fetch в Promise.all имеет собственный .catch(), возвращающий fallback-значение (null или []), внешний catch на строке 42-44 никогда не получит ошибку от этих запросов. Переменная error всегда останется null, и пользователь не увидит сообщение об ошибке, даже если все запросы завершатся неудачей.

🛠️ Предлагаемое исправление

Вариант 1: Убрать внешний try/catch и устанавливать error при проверке результатов:

 async function loadAll() {
   abortController?.abort()
   abortController = new AbortController()

   loading.value = true
   error.value = null

-  try {
-    const [analyticsData, recent, misses, retrieval] = await Promise.all([
-      fetchSearchAnalytics(abortController.signal).catch(() => null),
-      fetchRecentSearches(20, abortController.signal).catch(() => []),
-      fetchSearchMisses(abortController.signal).catch(() => []),
-      fetchRetrievalStats(undefined, abortController.signal).catch(() => null),
-    ])
-    analytics.value = analyticsData
-    recentQueries.value = recent || []
-    searchMisses.value = misses || []
-    retrievalStats.value = retrieval
-  } catch (err) {
-    if (err instanceof Error && err.name === 'AbortError') return
-    error.value = err instanceof Error ? err.message : 'Failed to load analytics'
-  } finally {
-    loading.value = false
-  }
+  const [analyticsData, recent, misses, retrieval] = await Promise.all([
+    fetchSearchAnalytics(abortController.signal).catch((e) => 
+      e.name === 'AbortError' ? null : { error: e }),
+    fetchRecentSearches(20, abortController.signal).catch((e) => 
+      e.name === 'AbortError' ? [] : { error: e }),
+    fetchSearchMisses(abortController.signal).catch((e) => 
+      e.name === 'AbortError' ? [] : { error: e }),
+    fetchRetrievalStats(undefined, abortController.signal).catch((e) => 
+      e.name === 'AbortError' ? null : { error: e }),
+  ])
+  
+  // Check if primary data failed
+  if (analyticsData && 'error' in analyticsData) {
+    error.value = 'Failed to load analytics'
+    loading.value = false
+    return
+  }
+  
+  analytics.value = analyticsData
+  recentQueries.value = Array.isArray(recent) ? recent : []
+  searchMisses.value = Array.isArray(misses) ? misses : []
+  retrievalStats.value = retrieval && !('error' in retrieval) ? retrieval : null
+  loading.value = false
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/AnalyticsView.vue` around lines 31 - 47, The per-promise
.catch() handlers swallow errors so the outer try/catch around Promise.all never
runs; remove the individual .catch() calls on fetchSearchAnalytics,
fetchRecentSearches, fetchSearchMisses, and fetchRetrievalStats so Promise.all
can reject and the outer catch can set error.value (and then use
nullish/coalesce when assigning analytics.value, recentQueries.value,
searchMisses.value, retrievalStats.value as needed); alternatively, if you want
per-call fallbacks plus overall error detection use Promise.allSettled and set
error.value when all results are rejected—update the code paths around
analytics.value, recentQueries.value, searchMisses.value, and
retrievalStats.value accordingly.
ui/src/views/TokensView.vue-188-189 (1)

188-189: ⚠️ Potential issue | 🟡 Minor

Риск случайного закрытия модального окна с созданным токеном.

Клик по backdrop (строка 189) вызывает closeCreateModal, даже когда отображается только что созданный токен. Пользователь может случайно закрыть окно, не скопировав токен, который больше не будет показан.

🛡️ Предлагаемое исправление
-        <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" `@click`="closeCreateModal" />
+        <div 
+          class="absolute inset-0 bg-black/60 backdrop-blur-sm" 
+          `@click`="!createdToken && closeCreateModal()" 
+        />

Или добавьте подтверждение перед закрытием, когда токен отображается.

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

In `@ui/src/views/TokensView.vue` around lines 188 - 189, Modify the backdrop
click behavior so clicking the overlay does not immediately call
closeCreateModal when the newly created token is being shown: add a boolean
flag/prop (e.g., createdTokenVisible or isShowingNewToken) tied to the
created-token state and replace the direct `@click`="closeCreateModal" with a
handler (e.g., onBackdropClick) that checks that flag and either prevents
closing (or opens a confirmation prompt) when createdTokenVisible is true,
otherwise calls closeCreateModal; update the TokensView.vue methods to implement
onBackdropClick and ensure showCreateModal and the created-token display logic
toggle the flag appropriately.
ui/src/router/index.ts-78-93 (1)

78-93: ⚠️ Potential issue | 🟡 Minor

Потенциальная проблема: начальное состояние аутентификации не проверено.

Навигационный guard проверяет loading.value, но при первой загрузке приложения loading и authenticated могут быть false до вызова checkAuth(). Это может вызвать мгновенное перенаправление на /login даже для аутентифицированных пользователей, пока App.vue не завершит проверку.

Рекомендуется гарантировать, что checkAuth() вызывается до первого разрешения маршрута.

🛡️ Предлагаемое исправление
+let authChecked = false
+
 router.beforeEach(async (to) => {
   const { authenticated, loading, checkAuth } = useAuth()

-  if (loading.value) {
+  if (loading.value || !authChecked) {
     await checkAuth()
+    authChecked = true
   }

   if (!to.meta.public && !authenticated.value) {
     return { name: 'login' }
   }

   if (to.name === 'login' && authenticated.value) {
     return { name: 'home' }
   }
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/router/index.ts` around lines 78 - 93, The guard in router.beforeEach
can return too early because loading.value and authenticated.value may both be
false initially; ensure checkAuth() is invoked and awaited before deciding
redirects by calling await checkAuth() whenever authentication hasn't been
initialized (e.g., when both loading.value and authenticated.value are false or
when a separate "initialized" flag is absent) so that the subsequent checks
against authenticated.value use the real state; update the guard around
useAuth(), checkAuth, loading and authenticated to guarantee checkAuth() runs
and completes before returning a route or redirect.
ui/src/components/layout/AppHeader.vue-22-32 (1)

22-32: ⚠️ Potential issue | 🟡 Minor

Отсутствует обработка ошибки от POST-запроса на рестарт.

Функция restartWorker не проверяет статус ответа от /api/update/restart. Если запрос вернёт ошибку (4xx/5xx), код всё равно будет ожидать готовности воркера.

🔧 Предлагаемое исправление
 const restartWorker = async () => {
   isRestarting.value = true
   try {
-    await fetch('/api/update/restart', { method: 'POST' })
+    const response = await fetch('/api/update/restart', { method: 'POST' })
+    if (!response.ok) {
+      throw new Error(`Restart request failed: ${response.status}`)
+    }
     await waitForWorker()
     globalThis.location.reload()
   } catch (error) {
     console.error('Failed to restart:', error)
     isRestarting.value = false
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/layout/AppHeader.vue` around lines 22 - 32, The
restartWorker function must check the POST response from
fetch('/api/update/restart') and handle non-OK status before awaiting
waitForWorker; update restartWorker to inspect the Response (e.g., response.ok
or response.status), throw or reject with the error body/message when the
response is not successful, and in the catch ensure isRestarting.value is set to
false and the error is logged; ensure references to restartWorker and
waitForWorker are used so the fix is applied in the correct function.
ui/src/components/search/SearchBar.vue-17-17 (1)

17-17: ⚠️ Potential issue | 🟡 Minor

localValue не синхронизируется при изменении modelValue родителем.

Если родительский компонент изменит modelValue (например, сбросит searchQuery в AppHeader.vue после навигации), localValue не обновится, так как отсутствует watch.

🔧 Предлагаемое исправление
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, watch, onMounted, onUnmounted } from 'vue'

 const localValue = ref(props.modelValue || '')

+watch(() => props.modelValue, (newVal) => {
+  if (newVal !== undefined && newVal !== localValue.value) {
+    localValue.value = newVal
+  }
+})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/search/SearchBar.vue` at line 17, localValue is initialized
from props.modelValue but never updates when the parent changes it; add a
watcher that observes props.modelValue (e.g. watch(() => props.modelValue, ...))
and updates localValue accordingly so the internal ref stays in sync with the
parent; ensure you reference the existing localValue and props.modelValue in the
watcher and keep existing emit/update logic (e.g. update:modelValue) intact so
two-way binding remains consistent.
internal/worker/handlers_analytics.go-53-58 (1)

53-58: ⚠️ Potential issue | 🟡 Minor

Некорректные значения days молча игнорируются.

Если параметр days содержит невалидное значение (нечисловое, ≤0 или >365), код молча использует значение по умолчанию вместо возврата ошибки 400. Это несогласованно с обработкой параметра period, где явно возвращается ошибка.

🔧 Предлагаемое исправление
 	days := 30
 	if val := r.URL.Query().Get("days"); val != "" {
-		if parsed, err := strconv.Atoi(val); err == nil && parsed > 0 && parsed <= 365 {
-			days = parsed
+		parsed, err := strconv.Atoi(val)
+		if err != nil {
+			http.Error(w, "invalid days: must be an integer", http.StatusBadRequest)
+			return
+		}
+		if parsed <= 0 || parsed > 365 {
+			http.Error(w, "invalid days: must be between 1 and 365", http.StatusBadRequest)
+			return
 		}
+		days = parsed
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_analytics.go` around lines 53 - 58, The code
silently falls back to default when the days query parameter is invalid; change
the parsing block for days (reading via r.URL.Query().Get("days") into variable
days and using strconv.Atoi) to mirror the period validation: if the parameter
is present but non-numeric, ≤0, or >365, return an HTTP 400 Bad Request with an
explanatory error (use http.Error or write status http.StatusBadRequest) instead
of silently using the default; keep the default only when the parameter is
absent.
ui/src/components/layout/AppHeader.vue-34-51 (1)

34-51: ⚠️ Potential issue | 🟡 Minor

waitForWorker не сигнализирует о тайм-ауте, что приводит к преждевременной перезагрузке страницы.

Если воркер не становится готовым за 30 попыток (15 секунд), функция завершается без индикации ошибки. В restartWorker это приведёт к вызову location.reload() при неготовом воркере.

🔧 Предлагаемое исправление
 const waitForWorker = async (maxAttempts = 30, delayMs = 500): Promise<void> => {
   for (let i = 0; i < maxAttempts; i++) {
     await new Promise(resolve => setTimeout(resolve, delayMs))
     try {
       const response = await fetch('/api/health', {
         signal: AbortSignal.timeout(2000),
       })
       if (response.ok) {
         const data = await response.json()
         if (data.status === 'ready') {
           return
         }
       }
     } catch {
       // Worker not ready yet, continue polling
     }
   }
+  throw new Error('Worker failed to become ready after restart')
 }

 const restartWorker = async () => {
   isRestarting.value = true
   try {
     await fetch('/api/update/restart', { method: 'POST' })
     await waitForWorker()
     globalThis.location.reload()
   } catch (error) {
     console.error('Failed to restart:', error)
     isRestarting.value = false
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/layout/AppHeader.vue` around lines 34 - 51, The
waitForWorker function currently swallows failures and simply returns after
exhausting attempts, so restartWorker cannot tell if the worker never became
ready; change waitForWorker to signal failure by throwing an Error (or returning
a rejected Promise) when the loop finishes without seeing data.status ===
'ready' (refer to waitForWorker), and update restartWorker to catch that
rejection (or inspect the boolean) before calling location.reload() so reload
only happens on an actual successful ready state; ensure the thrown Error
includes a clear message like "worker readiness timed out" to aid
logging/handling.
ui/src/composables/useObservation.ts-18-36 (1)

18-36: ⚠️ Potential issue | 🟡 Minor

Отменённый load() сбрасывает loading у нового запроса.

После abort() предыдущий вызов всё равно доходит до finally, поэтому быстрый повторный load() может скрыть состояние загрузки до завершения актуального запроса. Здесь нужен тот же guard по локальному controller/request id.

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

In `@ui/src/composables/useObservation.ts` around lines 18 - 36, The load function
currently aborts a previous request using the shared abortController, but the
previous request's finally block can still run and overwrite the new request's
state; to fix this, create a local const localController = new AbortController()
(or capture the signal) inside load, assign abortController = localController,
and then in the try/catch/finally only update observation.value, error.value and
loading.value if abortController === localController (or the localController was
not aborted) so aborted/old requests do not clear the latest loading state;
apply this guard in the catch (when checking for AbortError) and in finally so
only the active request manipulates loading.value and error.value.
ui/src/composables/usePatterns.ts-16-30 (1)

16-30: ⚠️ Potential issue | 🟡 Minor

Защитите loading от завершения отменённого запроса.

После abort() предыдущий loadPatterns() всё равно доходит до finally и может выставить loading = false уже во время нового запроса. При быстром повторном обновлении индикатор загрузки станет недостоверным; здесь лучше привязать сброс состояния к локальному controller, а не к общему полю.

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

In `@ui/src/composables/usePatterns.ts` around lines 16 - 30, loadPatterns
currently aborts a shared abortController and then always clears loading in its
finally block, which lets an earlier aborted call set loading=false during a
newer request; capture the current controller into a local variable (e.g., const
controller = abortController) before the await and use that local controller to
call fetchPatterns(controller.signal) and to decide in the finally block whether
to reset loading — only set loading.value = false (and/or clear error.value) if
controller === abortController (meaning this call is still the latest); update
references to abortController, loadPatterns, loading, and fetchPatterns
accordingly.
internal/worker/handlers_vault.go-40-42 (1)

40-42: ⚠️ Potential issue | 🟡 Minor

Отсутствует валидация параметра project.

В отличие от handleGetObservationsByTag, здесь параметр project не проверяется через ValidateProjectName, что может привести к path traversal или некорректным значениям.

🛡️ Предлагаемое исправление
 	project := r.URL.Query().Get("project")
+	if err := ValidateProjectName(project); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}

 	creds, err := s.observationStore.ListCredentials(r.Context(), project)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_vault.go` around lines 40 - 42, Validate the
incoming project query param before calling s.observationStore.ListCredentials:
call ValidateProjectName(project) (same validation used in
handleGetObservationsByTag), and if it returns an error respond with a 400 and
an explanatory message instead of proceeding; only call
s.observationStore.ListCredentials(r.Context(), project) when the project name
passes validation to prevent path traversal/invalid values.
internal/worker/handlers_vault.go-98-98 (1)

98-98: ⚠️ Potential issue | 🟡 Minor

Отсутствует валидация параметра project.

Аналогично handleListCredentials, параметр project не проверяется через ValidateProjectName.

🛡️ Предлагаемое исправление
 	project := r.URL.Query().Get("project")
+	if err := ValidateProjectName(project); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_vault.go` at line 98, The handler that reads project
:= r.URL.Query().Get("project") is missing validation; update the handler
function (the one that mirrors handleListCredentials in
internal/worker/handlers_vault.go) to call ValidateProjectName(project) after
extracting the query param, and if validation fails respond with a 400 error
(using http.Error or the existing error helper) and stop processing; ensure you
reuse the same validation flow/response style used in handleListCredentials so
invalid project names are rejected consistently.
internal/worker/handlers_vault.go-253-268 (1)

253-268: ⚠️ Potential issue | 🟡 Minor

Отсутствует валидация формата project.

Параметр project не проверяется через ValidateProjectName, как и в других обработчиках vault.

🛡️ Предлагаемое исправление
 	project := r.URL.Query().Get("project")
+	if err := ValidateProjectName(project); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
 	scope := r.URL.Query().Get("scope")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_vault.go` around lines 253 - 268, Добавьте валидацию
параметра project через существующую функцию ValidateProjectName: после
получения project := r.URL.Query().Get("project") и перед проверкой scope ==
"project" && project == "" вызовите ValidateProjectName(project) и при ошибке
верните http.StatusBadRequest с понятным сообщением (аналогично другим
обработчикам vault). Это обеспечит одинаковую проверку формата project в
handlers_vault.go и предотвратит некорректные имена проектов.
internal/worker/handlers_vault.go-181-184 (1)

181-184: ⚠️ Potential issue | 🟡 Minor

Отсутствует валидация формата project.

Параметр req.Project проверяется только на пустоту, но не валидируется через ValidateProjectName на корректность формата (path traversal, допустимые символы).

🛡️ Предлагаемое исправление
 	if req.Scope == "project" && req.Project == "" {
 		http.Error(w, "project is required for project-scoped credentials", http.StatusBadRequest)
 		return
 	}
+	if err := ValidateProjectName(req.Project); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_vault.go` around lines 181 - 184, В блоке проверки
scope в обработчике (где проверяются req.Scope и req.Project) добавьте валидацию
формата проекта: при req.Scope == "project" после проверки на пустоту вызовите
ValidateProjectName(req.Project) и, если он возвращает ошибку, ответьте
http.Error(w, "<validation message>", http.StatusBadRequest); тем самым
заменить/расширить текущую простую пустую проверку и использовать
ValidateProjectName для защиты от path traversal и недопустимых символов.
internal/worker/handlers_tags.go-133-161 (1)

133-161: ⚠️ Potential issue | 🟡 Minor

Параметр offset задокументирован, но не реализован.

В godoc (строка 133) указан параметр offset для пагинации, однако в коде обработчика он не парсится и не передаётся в SearchParams.

🔧 Предлагаемое исправление
 	limit := 50
 	if val := r.URL.Query().Get("limit"); val != "" {
 		if parsed, err := strconv.Atoi(val); err == nil && parsed > 0 && parsed <= 200 {
 			limit = parsed
 		}
 	}
+
+	offset := 0
+	if val := r.URL.Query().Get("offset"); val != "" {
+		if parsed, err := strconv.Atoi(val); err == nil && parsed >= 0 {
+			offset = parsed
+		}
+	}

 	if s.searchMgr == nil {
 		http.Error(w, "search manager not available", http.StatusServiceUnavailable)
 		return
 	}

 	searchParams := search.SearchParams{
 		Query:    tag,
 		Type:     "observations",
 		Project:  project,
 		Limit:    limit,
+		Offset:   offset,
 		Concepts: tag,
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_tags.go` around lines 133 - 161, The handler
handleGetObservationsByTag documents an "offset" query param but never parses or
uses it; update this function to read r.URL.Query().Get("offset"), parse it to
an int (e.g., strconv.Atoi), validate it is non-negative (and apply any
project-specific upper bound if needed, similar to limit), and pass the parsed
offset into the SearchParams (or equivalent struct) used when calling the
observation store search method so pagination actually works; ensure invalid
offsets return http.StatusBadRequest with a clear error message.
🧹 Nitpick comments (22)
ui/src/components/layout/ConfirmDialog.vue (1)

20-48: Рекомендуется улучшить доступность (a11y).

Модальное окно не имеет атрибутов role="dialog" и aria-modal="true", а также отсутствует управление фокусом (focus trap) и закрытие по клавише Escape. Это может затруднить использование компонента для пользователей с ограниченными возможностями.

♿ Предлагаемые улучшения доступности
         <!-- Dialog -->
-        <div class="relative glass border border-white/10 rounded-xl p-6 max-w-sm w-full shadow-2xl">
+        <div
+          role="dialog"
+          aria-modal="true"
+          :aria-labelledby="'confirm-title'"
+          class="relative glass border border-white/10 rounded-xl p-6 max-w-sm w-full shadow-2xl"
+          `@keydown.escape`="emit('cancel')"
+        >
-          <h3 class="text-lg font-semibold text-white mb-2">{{ title }}</h3>
+          <h3 id="confirm-title" class="text-lg font-semibold text-white mb-2">{{ title }}</h3>

Также рекомендуется добавить v-focus директиву или ref с focus() на кнопку отмены при открытии диалога.

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

In `@ui/src/components/layout/ConfirmDialog.vue` around lines 20 - 48, The
ConfirmDialog.vue markup lacks accessibility features: add role="dialog" and
aria-modal="true" to the dialog container (the element rendering title/message),
implement focus management by adding a ref (e.g., cancelButtonRef) to the Cancel
button (the element that calls emit('cancel')) and set focus to it when show
becomes true, and implement a focus trap inside the dialog so Tab/Shift+Tab
cycle within the dialog while open. Also add a keydown handler on the dialog (or
window while show is true) to listen for Escape and call emit('cancel'), and
ensure backdrop click remains wired to emit('cancel').
ui/src/components/observation/ObservationEditor.vue (1)

3-4: Неиспользуемые импорты.

TYPE_CONFIG и OBSERVATION_TYPES импортированы, но не используются в компоненте.

♻️ Предлагаемое исправление
-import type { Observation, ObservationType, ObservationScope } from '@/types'
-import { TYPE_CONFIG, OBSERVATION_TYPES } from '@/types/observation'
+import type { Observation, ObservationScope } from '@/types'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/observation/ObservationEditor.vue` around lines 3 - 4,
Удалите неиспользуемые импорты TYPE_CONFIG и OBSERVATION_TYPES из строки импорта
в ObservationEditor.vue (там же импортируются Observation, ObservationType и
ObservationScope) — откройте импорт, уберите упоминания TYPE_CONFIG и
OBSERVATION_TYPES или замените весь импорт на только используемые символы, чтобы
убрать предупреждение об неиспользуемых переменных.
internal/worker/token_stats.go (2)

70-72: Использование context.Background() вместо переданного контекста.

При вызове store.BatchIncrementStats используется context.Background(), что означает, что операция продолжит выполняться даже после отмены основного контекста (shutdown). Для финального flush это может быть желательно, но для регулярных flush по таймеру может привести к зависанию при остановке сервиса.

♻️ Предлагаемое исправление

Если требуется гарантированный flush при shutdown, можно использовать отдельный контекст с таймаутом:

-func (s *Service) flushTokenStats(counts map[string]int) {
+func (s *Service) flushTokenStats(ctx context.Context, counts map[string]int) {
 	if len(counts) == 0 {
 		return
 	}
 
 	s.initMu.RLock()
 	store := s.tokenStore
 	s.initMu.RUnlock()
 
 	if store == nil {
 		return
 	}
 
-	if err := store.BatchIncrementStats(context.Background(), counts); err != nil {
+	flushCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
+	defer cancel()
+	if err := store.BatchIncrementStats(flushCtx, counts); err != nil {
 		log.Warn().Err(err).Int("tokens", len(counts)).Msg("auth: failed to flush token stats")
 	}
 }

И обновить вызовы в startTokenStatsFlusher:

 case <-ctx.Done():
-	s.flushTokenStats(pending)
+	s.flushTokenStats(context.Background(), pending) // Final flush with background ctx
 	return
 
 case <-ticker.C:
-	s.flushTokenStats(pending)
+	s.flushTokenStats(ctx, pending)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/token_stats.go` around lines 70 - 72, The call to
store.BatchIncrementStats currently uses context.Background(), causing flushes
to ignore cancellation; change it to use the passed-in ctx (replace
context.Background() with the function's ctx) so regular timer-driven flushes
respect shutdown; for the special "final flush on shutdown" path in
startTokenStatsFlusher, create a short timeout context (context.WithTimeout) and
use that when invoking store.BatchIncrementStats to guarantee a bounded final
flush. Ensure you update the call sites that reference counts and
store.BatchIncrementStats accordingly.

45-50: Оптимизация очистки map.

Удаление элементов в цикле менее эффективно, чем создание нового map. При высокой нагрузке это может создавать дополнительную работу для GC.

♻️ Предлагаемое исправление
 			case <-ticker.C:
 				s.flushTokenStats(pending)
-				// Reset the map
-				for k := range pending {
-					delete(pending, k)
-				}
+				pending = make(map[string]int)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/token_stats.go` around lines 45 - 50, Вместо очистки map в
цикле, которая вызывает лишнюю работу GC, переинициализируй переменную pending
новым пустым map-ом после вызова s.flushTokenStats(pending); то есть в блоке,
где обрабатывается case <-ticker.C, вызывай s.flushTokenStats(pending) как
сейчас, затем присвой pending = make(<тип pending>) (используя тот же тип
ключа/значения, что у переменной pending) чтобы быстро создать пустую map; это
касается куска кода с s.flushTokenStats(pending) и переменной pending.
ui/src/views/AnalyticsView.vue (1)

101-105: Потенциальная ошибка при доступе к свойствам analytics.

Если fetchSearchAnalytics вернёт объект без некоторых свойств (например, avg_latency_ms будет undefined), вызов toFixed(1) приведёт к runtime-ошибке. Рекомендуется добавить проверку или использовать optional chaining с fallback.

♻️ Пример защитного подхода
-          <span class="text-2xl font-bold text-white font-mono">{{ analytics.avg_latency_ms.toFixed(1) }}<span class="text-sm text-slate-500">ms</span></span>
+          <span class="text-2xl font-bold text-white font-mono">{{ (analytics.avg_latency_ms ?? 0).toFixed(1) }}<span class="text-sm text-slate-500">ms</span></span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/AnalyticsView.vue` around lines 101 - 105, The template directly
calls analytics.avg_latency_ms.toFixed(1) which will throw if avg_latency_ms is
undefined; update the component (AnalyticsView.vue) to guard this access by
using optional chaining with a fallback or a computed property—e.g., compute a
safeAvgLatency in the component (derived from analytics and fetchSearchAnalytics
results) that returns a number or null, and then render
safeAvgLatency?.toFixed(1) ?? '-' (or (safeAvgLatency ?? 0).toFixed(1)) in the
template; ensure you reference analytics.avg_latency_ms and the
fetchSearchAnalytics result when populating the computed value so the template
never calls toFixed on undefined.
ui/src/views/HomeView.vue (1)

2-7: Дублирующийся импорт useSSE.

useSSE импортируется дважды: на строке 2 (из @/composables) и на строке 7 (снова из @/composables). Это избыточно.

♻️ Предлагаемое исправление
-import { useStats, useTimeline, useHealth } from '@/composables'
+import { useStats, useTimeline, useHealth, useSSE } from '@/composables'
 import StatsCards from '@/components/StatsCards.vue'
 import Sidebar from '@/components/Sidebar.vue'
 import FilterTabs from '@/components/FilterTabs.vue'
 import Timeline from '@/components/Timeline.vue'
-import { useSSE } from '@/composables'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/HomeView.vue` around lines 2 - 7, The file HomeView.vue imports
useSSE twice (from '@/composables') — remove the duplicate import so useSSE is
only imported once; locate the duplicate import lines referencing useSSE in the
import block (symbols: useSSE, useStats, useTimeline, useHealth, StatsCards,
Sidebar, FilterTabs, Timeline) and delete the redundant useSSE import or merge
it into the existing import statement to avoid the duplicated symbol.
ui/src/components/layout/AppSidebar.vue (2)

49-52: Обработка ошибок при logout.

Если logout() выбросит исключение (например, сетевая ошибка), пользователь не будет перенаправлен на страницу входа. Возможно, стоит перенаправлять в любом случае.

♻️ Предлагаемое исправление
 async function handleLogout() {
-  await logout()
-  router.push({ name: 'login' })
+  try {
+    await logout()
+  } finally {
+    router.push({ name: 'login' })
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/layout/AppSidebar.vue` around lines 49 - 52, The
handleLogout function currently awaits logout() and only navigates on success;
wrap the logout() call in try/catch (or use try/finally) so any exception is
caught and the user is still redirected by calling router.push({ name: 'login'
}) in the finally (or in both success and catch paths); also log the error
(e.g., console.error or app logger) inside the catch and optionally show a
user-friendly notification, referencing the handleLogout function and the logout
and router.push calls.

42-47: Потенциально некорректное сопоставление активного маршрута.

Использование startsWith может привести к ложным срабатываниям. Например, если добавится маршрут /tokens-admin, он будет подсвечиваться при нахождении на /tokens. Рекомендуется использовать точное совпадение или проверку по сегментам пути.

♻️ Предлагаемое исправление
 function isActive(item: NavItem): boolean {
   if (item.path === '/') {
     return route.path === '/'
   }
-  return route.path.startsWith(item.path)
+  return route.path === item.path || route.path.startsWith(item.path + '/')
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/layout/AppSidebar.vue` around lines 42 - 47, The isActive
function is using startsWith which can falsely match routes (e.g., item.path
'/tokens' matching '/tokens-admin'); update isActive (the function that takes
NavItem and reads route.path) to only treat an item as active when the paths are
exactly equal or when the route path continues with a path-segment boundary (for
example check route.path === item.path OR route.path.startsWith(item.path + '/')
), keeping the special-case for '/' unchanged; implement this change inside
isActive so submenu and edge routes no longer produce false positives.
ui/src/composables/usePagination.ts (1)

58-61: Отсутствует валидация в setOffset.

Функция setOffset не проверяет валидность значения newOffset. Отрицательные значения или значения, превышающие total, могут привести к некорректному состоянию пагинации.

♻️ Предлагаемое исправление
   function setOffset(newOffset: number) {
+    if (newOffset < 0) {
+      newOffset = 0
+    }
     offset.value = newOffset
     fetchPage()
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/composables/usePagination.ts` around lines 58 - 61, The setOffset
function currently assigns offset.value directly and calls fetchPage() without
validation; update setOffset to validate and clamp newOffset to a safe range
before applying it (e.g., ensure newOffset is at least 0 and at most Math.max(0,
total - 1) or the computed max offset based on pageSize/total if pageSize is
available), then set offset.value to the clamped value and call fetchPage();
reference the setOffset function, the offset reactive ref, total (and pageSize
or totalPages if present), and fetchPage to locate and implement the check.
ui/src/composables/useTokens.ts (1)

29-39: Рассмотрите передачу AbortSignal в createToken.

В отличие от loadTokens, функция create не использует AbortController для отмены запроса. При размонтировании компонента во время создания токена запрос продолжится.

♻️ Предлагаемое улучшение
 async function create(name: string, scope: string): Promise<CreateTokenResponse> {
   error.value = null
   try {
-    const result = await createToken({ name, scope })
+    const result = await createToken({ name, scope }, abortController?.signal)
     await loadTokens()
     return result
   } catch (err) {
+    if (err instanceof Error && err.name === 'AbortError') throw err
     error.value = err instanceof Error ? err.message : 'Failed to create token'
     throw err
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/composables/useTokens.ts` around lines 29 - 39, The create function
should support cancellation like loadTokens by accepting/creating an
AbortController and passing its signal into createToken; update the create
function (and any callers of createToken) to call createToken({ name, scope,
signal }) and ensure the controller is aborted on component unmount or when
appropriate (mirror the existing AbortController handling used in loadTokens) so
in-flight create requests are cancelled when the component is destroyed.
ui/src/components/observation/TagEditor.vue (1)

71-86: Рекомендация: кэшировать конфигурацию концепта для устранения повторных вызовов.

getConceptConfig(concept) вызывается 4 раза для каждого концепта в цикле. Это незначительное снижение производительности, но можно оптимизировать.

♻️ Предлагаемое улучшение
-      <span
-        v-for="concept in concepts"
-        :key="concept"
-        class="inline-flex items-center gap-1 group"
-      >
-        <Badge
-          :icon="getConceptConfig(concept).icon"
-          :color-class="getConceptConfig(concept).colorClass"
-          :bg-class="getConceptConfig(concept).bgClass"
-          :border-class="getConceptConfig(concept).borderClass"
-        >
+      <span
+        v-for="concept in concepts"
+        :key="concept"
+        class="inline-flex items-center gap-1 group"
+      >
+        <Badge
+          v-bind="(() => {
+            const cfg = getConceptConfig(concept)
+            return { icon: cfg.icon, colorClass: cfg.colorClass, bgClass: cfg.bgClass, borderClass: cfg.borderClass }
+          })()"
+        >

Или создайте computed property с маппингом концептов на их конфигурации.

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

In `@ui/src/components/observation/TagEditor.vue` around lines 71 - 86,
getConceptConfig(concept) is being called repeatedly inside the Badge rendering;
cache the result once per loop iteration (or create a computed mapping) and
reuse it to avoid 4 redundant calls per concept. For example, compute a local
const/config object from getConceptConfig(concept) before rendering the Badge
(or add a computed property mapping concepts to configs) and then reference that
single object for icon, colorClass, bgClass, and borderClass; keep the existing
removeTag, updating and concept bindings unchanged.
ui/src/views/SearchView.vue (1)

10-10: Неиспользуемая функция clear из деструктуризации.

clear извлекается из useSearch(), но нигде не используется в компоненте. Рассмотрите удаление для чистоты кода или добавление кнопки очистки поиска.

♻️ Предлагаемое исправление
-const { query, project, results, totalCount, loading, error, decisionMode, intent, search, clear } = useSearch()
+const { query, project, results, totalCount, loading, error, decisionMode, intent, search } = useSearch()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/SearchView.vue` at line 10, В деструктуризации const { query,
project, results, totalCount, loading, error, decisionMode, intent, search,
clear } = useSearch() свойство clear не используется — удалите clear из списка,
чтобы убрать неиспользуемую переменную, или если предусматривалась
функциональность очистки, добавьте соответствующую кнопку/обработчик, который
вызывает clear() (найдите useSearch и компонент SearchView.vue, обновите шаблон
и/или методы, чтобы либо убрать clear из деструктуризации, либо подключить
clear() к UI).
ui/src/views/SessionsView.vue (1)

24-31: Несогласованное поведение поиска и фильтрации.

setProject автоматически вызывает loadSessions(), а handleSearch требует нажатия Enter. При изменении дат также вызывается loadSessions(). Рассмотрите возможность унификации поведения: либо поиск также запускается автоматически с debounce, либо все фильтры требуют явного подтверждения.

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

In `@ui/src/views/SessionsView.vue` around lines 24 - 31, Несогласованное
поведение: setProject вызывает loadSessions() сразу, а handleSearch() вызывает
только search() по Enter—унифицируй поведение; либо изменяй handleSearch чтобы
вызывал loadSessions() автоматически с debounce (например 300–500ms) вместо
требовать Enter, либо убери автоматический вызов loadSessions() из setProject и
вызови loadSessions() только по явному подтверждению пользователем; обнови также
обработчики изменения дат, чтобы они следовали той же логике; изменения должны
затронуть функции handleSearch, setProject, loadSessions и реактивные фильтры
(filterProject и обработчики дат).
ui/src/views/VaultView.vue (1)

72-81: Рассмотрите отмену асинхронных операций при размонтировании компонента.

Если компонент размонтируется во время выполнения handleDelete, вызов removeCredential продолжится и попытается обновить состояние размонтированного компонента. Это не критично, но может вызвать предупреждения в консоли.

♻️ Возможное улучшение
+const isMounted = ref(true)
+
+onUnmounted(() => {
+  isMounted.value = false
+  stopTick()
+})
-onUnmounted(() => stopTick())

 async function handleDelete() {
   if (!deleteTarget.value) return
   showDeleteConfirm.value = false
   try {
     await removeCredential(deleteTarget.value)
   } catch {
     // Error handled by composable
   }
-  deleteTarget.value = null
+  if (isMounted.value) {
+    deleteTarget.value = null
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/VaultView.vue` around lines 72 - 81, handleDelete can continue
running after the component unmounts and then call removeCredential and mutate
refs (deleteTarget, showDeleteConfirm) on an unmounted component; to fix, make
the async operation cancellable or guard post-await state updates: create an
AbortController or a mounted flag in the setup() and pass a cancellation token
to removeCredential (or check the mounted flag) and only update
showDeleteConfirm.value and deleteTarget.value if not cancelled/unmounted;
ensure removeCredential accepts and respects an AbortSignal or that you catch
AbortError and skip state updates in handleDelete and in the composable.
internal/worker/handlers_analytics.go (2)

60-61: Магическое число days*50 требует документации или пересмотра.

Множитель 50 для лимита выборки не обоснован. Если за день создаётся больше 50 наблюдений, данные будут неполными. Рекомендуется либо добавить комментарий с обоснованием, либо использовать более надёжный подход (например, фильтрацию по дате на уровне запроса).

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

In `@internal/worker/handlers_analytics.go` around lines 60 - 61, The magic
multiplier days*50 used when calling s.observationStore.GetRecentObservations
should be removed or documented and replaced with a robust date-based query;
update the call site in handlers_analytics.go (where obs, err :=
s.observationStore.GetRecentObservations(r.Context(), project, days*50) is
invoked) to compute an explicit cutoff time (e.g. cutoff :=
time.Now().Add(-time.Duration(days)*24*time.Hour)) and either call a store
method that accepts a time range (e.g. GetObservationsSince or extend
GetRecentObservations to accept a since timestamp) or pass a clearly named limit
variable with a comment explaining any non-obvious choice, ensuring observations
are filtered by created_at >= cutoff rather than relying on an arbitrary
multiplier.

126-135: Рассмотрите использование slices.SortFunc из стандартной библиотеки Go.

Ручная реализация частичной сортировки работает, но менее читабельна. Начиная с Go 1.21+ доступен slices.SortFunc, который делает код понятнее.

♻️ Предлагаемый рефакторинг
+import "slices"
+
 	// Sort and get top concepts
-	type conceptEntry struct {
-		name  string
-		count int
-	}
-	var topConcepts []conceptEntry
-	for name, count := range conceptCounts {
-		topConcepts = append(topConcepts, conceptEntry{name, count})
-	}
-	for i := 0; i < len(topConcepts) && i < 10; i++ {
-		for j := i + 1; j < len(topConcepts); j++ {
-			if topConcepts[j].count > topConcepts[i].count {
-				topConcepts[i], topConcepts[j] = topConcepts[j], topConcepts[i]
-			}
-		}
-	}
-	if len(topConcepts) > 10 {
-		topConcepts = topConcepts[:10]
-	}
+	type conceptEntry struct {
+		name  string
+		count int
+	}
+	topConcepts := make([]conceptEntry, 0, len(conceptCounts))
+	for name, count := range conceptCounts {
+		topConcepts = append(topConcepts, conceptEntry{name, count})
+	}
+	slices.SortFunc(topConcepts, func(a, b conceptEntry) int {
+		return b.count - a.count // descending
+	})
+	if len(topConcepts) > 10 {
+		topConcepts = topConcepts[:10]
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_analytics.go` around lines 126 - 135, Replace the
manual nested-loop partial sort over topConcepts with the standard library
slices.SortFunc: import "slices" and call slices.SortFunc(topConcepts, func(a, b
YourType) bool { return a.count > b.count }) (use the actual element type of
topConcepts) to sort by count descending, then keep the existing truncation
topConcepts = topConcepts[:min(len(topConcepts), 10)] (or the current if-check).
Update imports accordingly and ensure the comparator uses the correct field name
`count`.
ui/src/views/ObservationDetailView.vue (1)

2-2: Неиспользуемый импорт onUnmounted.

onUnmounted импортируется, но не используется в компоненте. Вероятно, это осталось после рефакторинга, так как для очистки listener'а используется onBeforeUnmount.

🧹 Предлагаемое исправление
-import { ref, computed, onMounted, onUnmounted, onBeforeUnmount } from 'vue'
+import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/ObservationDetailView.vue` at line 2, The import list contains
an unused symbol onUnmounted; remove onUnmounted from the import statement
(leave ref, computed, onMounted, onBeforeUnmount) in the
ObservationDetailView.vue import line so only actually used lifecycle hooks
(onMounted, onBeforeUnmount) are imported.
ui/src/composables/useVault.ts (1)

76-81: Оптимистичное обновление credential_count может привести к отрицательному значению.

Если credential_count равен 0 (например, из-за рассинхронизации), декремент приведёт к -1. Хотя маловероятно, рекомендуется добавить защиту.

🛡️ Предлагаемое исправление
       if (vaultStatus.value) {
         vaultStatus.value = {
           ...vaultStatus.value,
-          credential_count: vaultStatus.value.credential_count - 1,
+          credential_count: Math.max(0, vaultStatus.value.credential_count - 1),
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/composables/useVault.ts` around lines 76 - 81, The optimistic
decrement of vaultStatus.value.credential_count can produce a negative number;
update the block in useVault.ts that touches vaultStatus (vaultStatus.value) to
clamp or floor the new credential_count at 0 instead of blindly subtracting 1
(e.g., compute newCount = Math.max(0, vaultStatus.value.credential_count - 1)
and assign that back into vaultStatus.value), ensuring the guard runs only when
vaultStatus.value is present and preserving other fields via the existing
spread.
ui/src/views/ObservationsView.vue (1)

95-107: Клиентская фильтрация на странице может приводить к пустым страницам.

Если сервер возвращает 20 записей, но все они отфильтровываются по currentType или currentConcept, пользователь увидит пустую страницу, хотя данные есть на других страницах. Рекомендуется применять фильтры на сервере или отображать предупреждение.

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

In `@ui/src/views/ObservationsView.vue` around lines 95 - 107,
filteredObservations is doing client-side filtering on the paginated
observations (observations.value) using currentType.value and
currentConcept.value which can produce empty pages even though matching items
exist on other pages; update the code so filters are applied server-side by
including currentType and currentConcept in the API request that loads
observations (or call the existing fetch/load function whenever
currentType.value or currentConcept.value changes) and remove or simplify
client-side filtering, and additionally handle the case where a page returns no
items by resetting/adjusting the current page (or fetching page 1) or showing a
user-visible warning; reference filteredObservations, observations.value,
currentType.value, currentConcept.value and the fetch/load method you use to
load paginated observations when implementing this change.
internal/worker/handlers_auth.go (3)

95-102: Рассмотрите добавление флага Secure для cookie в production.

Cookie устанавливается с HttpOnly и SameSite=Strict, что хорошо. Однако флаг Secure отсутствует, что позволяет передавать cookie по HTTP. Для production-окружения рекомендуется добавить Secure: true или сделать его условным на основе конфигурации.

🔒 Предлагаемое исправление
 	http.SetCookie(w, &http.Cookie{
 		Name:     sessionCookieName,
 		Value:    cookieValue,
 		Path:     "/",
 		MaxAge:   sessionMaxAge,
 		HttpOnly: true,
 		SameSite: http.SameSiteStrictMode,
+		Secure:   r.TLS != nil, // или использовать конфигурацию
 	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_auth.go` around lines 95 - 102, The cookie set in
the http.SetCookie call is missing the Secure flag; update the cookie
construction (the http.SetCookie call that uses sessionCookieName, cookieValue
and sessionMaxAge) to include Secure: true in production (or conditionally set
Secure based on your runtime configuration/environment), e.g., read the app
environment flag/config and set Secure accordingly so the session cookie is only
sent over HTTPS in production.

358-370: Рассмотрите использование strings.Contains для читаемости.

Ручная реализация поиска подстроки менее читаема, чем стандартная функция strings.Contains.

♻️ Предлагаемое упрощение
+import "strings"
+
 // containsDuplicateKey checks error message for duplicate key indicators.
 func containsDuplicateKey(msg string) bool {
-	for _, s := range []string{"duplicate key", "23505", "UNIQUE constraint"} {
-		if len(msg) >= len(s) {
-			for i := 0; i <= len(msg)-len(s); i++ {
-				if msg[i:i+len(s)] == s {
-					return true
-				}
-			}
-		}
+	for _, pattern := range []string{"duplicate key", "23505", "UNIQUE constraint"} {
+		if strings.Contains(msg, pattern) {
+			return true
+		}
 	}
 	return false
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_auth.go` around lines 358 - 370, The manual
substring search in containsDuplicateKey is verbose and should be replaced with
the standard strings.Contains for readability and correctness; update
containsDuplicateKey to iterate over the slice of indicators using
strings.Contains(msg, s) and add the import "strings" if not already present so
the function returns true when any indicator is found and false otherwise.

318-322: Все ошибки возвращают 404, что может скрывать реальные проблемы.

При ошибке tokenStore.Revoke всегда возвращается 404, даже если ошибка связана с недоступностью БД или другими внутренними проблемами. Это может затруднить диагностику.

♻️ Предлагаемое исправление с разграничением ошибок
 	if err := tokenStore.Revoke(r.Context(), id); err != nil {
 		log.Error().Err(err).Str("token_id", id).Msg("auth: failed to revoke token")
-		http.Error(w, "not found", http.StatusNotFound)
+		// Можно проверить тип ошибки для более точного статуса
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			http.Error(w, "not found", http.StatusNotFound)
+		} else {
+			http.Error(w, "internal error", http.StatusInternalServerError)
+		}
 		return
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_auth.go` around lines 318 - 322, The Revoke error
handling always returns 404 which hides real failures; update the block around
tokenStore.Revoke(id) to distinguish "not found" from other errors by checking
against the token store's sentinel not-found error (e.g., tokenStore.ErrNotFound
or errors.Is(err, sql.ErrNoRows)) and only return http.StatusNotFound for that
case, otherwise log the error with Err(err).Str("token_id", id) and return
http.StatusInternalServerError; keep the existing log call but ensure messages
accurately reflect the error type and do not map all failures to 404.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f4e1546c-56c0-4d55-befd-486e1d0b3af1

📥 Commits

Reviewing files that changed from the base of the PR and between a8a3fe9 and a0bcc87.

📒 Files selected for processing (53)
  • internal/db/gorm/migrations.go
  • internal/db/gorm/models.go
  • internal/db/gorm/token_store.go
  • internal/worker/handlers_analytics.go
  • internal/worker/handlers_auth.go
  • internal/worker/handlers_maintenance.go
  • internal/worker/handlers_sessions_rest.go
  • internal/worker/handlers_tags.go
  • internal/worker/handlers_vault.go
  • internal/worker/middleware.go
  • internal/worker/service.go
  • internal/worker/token_stats.go
  • ui/package.json
  • ui/src/App.vue
  • ui/src/components/layout/AppHeader.vue
  • ui/src/components/layout/AppSidebar.vue
  • ui/src/components/layout/ConfirmDialog.vue
  • ui/src/components/layout/EmptyState.vue
  • ui/src/components/layout/Pagination.vue
  • ui/src/components/observation/ObservationEditor.vue
  • ui/src/components/observation/TagEditor.vue
  • ui/src/components/search/SearchBar.vue
  • ui/src/components/search/SearchResults.vue
  • ui/src/composables/index.ts
  • ui/src/composables/useAuth.ts
  • ui/src/composables/useLogs.ts
  • ui/src/composables/useObservation.ts
  • ui/src/composables/usePagination.ts
  • ui/src/composables/usePatterns.ts
  • ui/src/composables/useSearch.ts
  • ui/src/composables/useSessions.ts
  • ui/src/composables/useTokens.ts
  • ui/src/composables/useVault.ts
  • ui/src/main.ts
  • ui/src/router/index.ts
  • ui/src/types/index.ts
  • ui/src/types/observation.ts
  • ui/src/types/search.ts
  • ui/src/utils/api.ts
  • ui/src/views/AnalyticsView.vue
  • ui/src/views/GraphView.vue
  • ui/src/views/HomeView.vue
  • ui/src/views/LoginView.vue
  • ui/src/views/LogsView.vue
  • ui/src/views/ObservationDetailView.vue
  • ui/src/views/ObservationsView.vue
  • ui/src/views/PatternsView.vue
  • ui/src/views/SearchView.vue
  • ui/src/views/SessionsView.vue
  • ui/src/views/SystemView.vue
  • ui/src/views/TokensView.vue
  • ui/src/views/VaultView.vue
  • ui/tailwind.config.js

Comment thread internal/db/gorm/token_store.go Outdated
Comment thread internal/worker/handlers_maintenance.go
Comment thread internal/worker/handlers_sessions_rest.go
Comment thread internal/worker/handlers_sessions_rest.go
Comment thread ui/src/components/observation/ObservationEditor.vue
Comment thread ui/src/utils/api.ts
Comment thread ui/src/utils/api.ts Outdated
Comment thread ui/src/utils/api.ts
Comment thread ui/src/views/ObservationsView.vue
Comment thread ui/src/views/SystemView.vue
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant set of new features, primarily adding backend APIs and corresponding UI views for a comprehensive dashboard. The new functionality includes API token management, a secure vault, log streaming, analytics, and system maintenance endpoints. The authentication system has been robustly expanded to support master tokens, client API tokens, and session cookies.

My review identified one critical bug in the frontend API client for tagging observations, which would prevent the feature from working correctly. I also found a couple of medium-severity issues in the backend related to code efficiency and robustness. Specifically, I've suggested improvements for an inefficient sorting algorithm and a brittle method of parsing database error messages.

Overall, this is a well-architected and substantial contribution. The backend code is generally solid, and the UI refactoring to a multi-page application is a major improvement. After addressing the identified issues, this pull request will be a great addition.

Comment thread ui/src/utils/api.ts Outdated
Comment thread internal/worker/handlers_analytics.go Outdated
Comment thread internal/worker/handlers_auth.go
Comment thread ui/src/utils/api.ts
@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 19, 2026

@codex review

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 19, 2026

🤖 PR Review MCP State (auto-managed, do not edit)
{
  "version": 2,
  "parentChildren": {},
  "resolvedNitpicks": {
    "coderabbit-nitpick-0303c523-95": {
      "resolvedAt": "2026-03-19T05:34:35.517Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-71f12b56-358": {
      "resolvedAt": "2026-03-19T05:34:40.954Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-cff407bf-318": {
      "resolvedAt": "2026-03-19T05:34:45.870Z",
      "resolvedBy": "agent"
    }
  },
  "updatedAt": "2026-03-19T05:34:46.274Z"
}

thebtf added 2 commits March 19, 2026 08:37
Backend fixes:
- handlers_auth.go: add Secure cookie flag (r.TLS != nil), discriminate
  Revoke errors (404 vs 500), simplify containsDuplicateKey with strings.Contains
- handlers_analytics.go: use slices.SortFunc for top concepts, add heuristic comment
- handlers_maintenance.go: use context.Background() for async RunNow call
- handlers_sessions_rest.go: cap sessions limit at 200, sanitize 500 error messages
- middleware.go: fix FindByPrefix prefix-collision — iterate all candidates with bcrypt
- token_store.go: FindByPrefix returns []APIToken to handle collisions
- token_stats.go: pass ctx to flushTokenStats, use context.WithTimeout for shutdown flush

Frontend fixes:
- api.ts: unwrap tokens response, rename revokeToken param id, fix updateObservationTags payload
- useAuth.ts: add credentials: include to all fetch calls, catch network errors in login
- useTokens.ts: revoke by id, createToken passes signal
- useLogs.ts: track reconnectTimer, disconnect() cancels pending reconnects
- usePagination.ts: clamp setOffset to [0, maxOffset]
- useVault.ts: guard credential_count decrement with Math.max(0, ...)
- ObservationEditor.vue: fix array mutation in computed/handleSave with spread sort
- ObservationEditor.vue: remove unused TYPE_CONFIG/OBSERVATION_TYPES imports
- ObservationDetailView.vue: remove unused onUnmounted import
- SearchResults.vue: replace div with RouterLink for keyboard accessibility
- TagEditor.vue: pre-map conceptConfigs computed, avoid repeated getConceptConfig calls
- ConfirmDialog.vue: add role=dialog, aria-modal, aria-labelledby, Escape key handler
- AppSidebar.vue: fix isActive for nested routes, guard handleLogout with try/finally
- ObservationsView.vue: remove Delete button (no backend endpoint), fix batch action type
- TokensView.vue: use token.id for key/revoke, display token_prefix and last_used_at
- AnalyticsView.vue: guard all .toFixed() calls with ?? 0
- HomeView.vue: merge duplicate useSSE import
- SearchView.vue: remove unused clear from useSearch destructuring
- SessionsView.vue: setProject calls search() for consistency
- VaultView.vue: guard post-await state with isMounted flag
- SystemView.vue: replace Promise.all with Promise.allSettled, surface real errors
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant