Skip to content

feat: Dashboard Analytics v2 — persistent analytics, bugfixes, verified facts TTL#26

Merged
thebtf merged 10 commits into
mainfrom
feat/dashboard-analytics-v2
Mar 20, 2026
Merged

feat: Dashboard Analytics v2 — persistent analytics, bugfixes, verified facts TTL#26
thebtf merged 10 commits into
mainfrom
feat/dashboard-analytics-v2

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Mar 20, 2026

Summary

  • Phase 1: Fix all broken dashboard pages (Patterns, Graph, Analytics, Sessions)
  • Phase 2: Persistent search query log (replaces in-memory ring buffer)
  • Phase 4: Enhanced analytics dashboard with TimeRangeSelector
  • Phase 5: Verified facts TTL on observations (expires_at, auto-TTL by tags)

Changes

Phase 1: Fix Broken UI

  • Fix Pattern type: occurrencesfrequency, add last_seen_at fields
  • Fix PatternsView sort by frequency/last_seen_at_epoch
  • Fix Graph vis-network: double nextTick, container size check, App.vue min-h-0
  • Make search misses project optional (global aggregation)
  • Sessions: show specific message for 503 (indexing not configured)

Phase 2: Persistent Search Query Log

  • Migration 037: search_query_log table with indexes
  • SearchQueryLogStore: async insert, analytics aggregation, recent queries, cleanup
  • Rewritten /api/search/analytics and /api/search/recent to query from DB
  • 90-day cleanup in maintenance handler
  • trackSearchQuery now persists to DB alongside in-memory ring buffer

Phase 4: Enhanced Analytics Dashboard

  • New TimeRangeSelector component (today/7d/30d/all)
  • Client-side timezone offset in ISO8601 since parameter
  • AnalyticsView uses TimeRangeSelector, empty states per section
  • API functions updated with since support

Phase 5: Verified Facts TTL

  • Migration 039: expires_at TIMESTAMPTZ NULL + ttl_days INT NULL on observations
  • store_memory MCP tool: new ttl_days parameter with auto-TTL by tags
  • Auto-TTL rules: api→7d, library→30d, language-feature→90d, architecture→180d
  • is_expired computed in toModelObservation (real-time, not materialized)
  • Maintenance job: monitors expired verified facts (log-only, no mutations)

Other

  • Fix marketplace source path
  • Fix README token name (ENGRAM_AUTH_ADMIN_TOKEN)
  • Fix config auth token priority

Migrations

  • 037: search_query_log table
  • 038: retrieval_stats_log table (schema only — batched flusher deferred)
  • 039: observations expires_at/ttl_days columns

Deferred (Phase 3)

  • RetrievalStatsLogger with batched flush (T3.2-T3.5) — table created, wiring deferred
  • MCP tool search query tracking — REST endpoints covered, MCP tools not yet

Test plan

  • Patterns page: sort by frequency shows different order than confidence
  • Graph page: vis-network renders nodes (at least 1 visible)
  • Analytics: search misses populated without project filter
  • Analytics: TimeRangeSelector filters data (today vs all)
  • Sessions: shows "Session indexing not configured" on 503
  • After server restart: search analytics shows non-zero data
  • store_memory with verified+api tags: expires_at set to +7 days
  • store_memory with ttl_days=180: agent override works
  • Existing observations: expires_at = NULL after migration

Summary by CodeRabbit

Примечания к выпуску

  • Новые функции

    • Постоянное логирование поисковых запросов и расширенная аналитика с выбором периода
    • Поддержка TTL для сохранённых наблюдений; управление TTL при сохранении
    • Новый компонент выбора временного диапазона в аналитике
  • Улучшения

    • Улучшена прокрутка основной области, рендер графов и поведение загрузки
    • Клиентская сортировка и отображение статистики обновлены для частоты и времени
  • Обновления конфигурации

    • Переименовано рекомендованное окруж. переменных токена аутентификации (новый рекомендуемый ключ)

thebtf added 8 commits March 20, 2026 03:02
…TL fields

- Migration 037: search_query_log table with indexes
- Migration 038: retrieval_stats_log table with indexes
- Migration 039: observations expires_at/ttl_days columns with partial index
- SearchQueryLogStore: async insert, analytics aggregation, recent queries, cleanup
- Observation model: ExpiresAt, TtlDays, IsExpired (computed) fields
- toModelObservation: maps expires_at/ttl_days, computes is_expired at query time
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

Warning

Rate limit exceeded

@thebtf has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e88fce7-4b8f-42bd-abb3-02e71b667a67

📥 Commits

Reviewing files that changed from the base of the PR and between c9aa12d and 8c56f27.

📒 Files selected for processing (12)
  • README.md
  • internal/db/gorm/observation_store.go
  • internal/db/gorm/search_query_log_store.go
  • internal/maintenance/service.go
  • internal/mcp/server.go
  • internal/mcp/tools_memory.go
  • internal/worker/handlers_context.go
  • internal/worker/handlers_data.go
  • internal/worker/service.go
  • pkg/models/observation.go
  • ui/src/components/TimeRangeSelector.vue
  • ui/src/views/AnalyticsView.vue

Walkthrough

Добавлены персистентное логирование поисковых запросов и аналитика, миграции БД (включая TTL для наблюдений), поддержка TTL в хранении наблюдений и инструменте store_memory, расширены HTTP-обработчики и UI для выбора временных диапазонов; также переименована предпочитаемая env-переменная аутентификации.

Changes

Cohort / File(s) Summary
Манифест и документация
\.claude-plugin/marketplace.json, README.md
Обновлён источник плагина и переименована рекомендуемая переменная окружения для токена (ENGRAM_AUTH_ADMIN_TOKEN), старый алиас сохраняется как deprecated.
Конфигурация
internal/config/config.go
Load/GetWorkerToken теперь предпочитают ENGRAM_AUTH_ADMIN_TOKEN, с откатом к ENGRAM_API_TOKEN при отсутствии.
Миграции БД
internal/db/gorm/migrations.go
Добавлены миграции 037, 038, 039: таблицы search_query_log, retrieval_stats_log и добавление TTL-полей к observations с индексами.
Модели и магазины наблюдений
internal/db/gorm/models.go, pkg/models/observation.go, internal/db/gorm/observation_store.go
Добавлены поля ExpiresAt и TtlDays в модель Observation; преобразования в store учитывают эти поля, добавлен SetObservationTTL и вычисление IsExpired; GetSearchMissStats теперь агрегирует по всем проектам при пустом project.
Поисковый лог (персистентный)
internal/db/gorm/search_query_log_store.go
Новый SearchQueryLogStore: async LogQuery, GetAnalytics(since), GetRecent(project,limit), Cleanup(olderThan). Моделирование и индексы соответствуют миграции.
Сервис обслуживания и планирование
internal/maintenance/service.go, internal/worker/service.go, internal/worker/handlers_maintenance.go
Добавлена задача подсчёта истёкших verified-наблюдений; интеграция логирования запросов в трекинг поиска; фоновая очистка логов (90 дней) при запуске maintenance.
MCP / инструменты памяти
internal/mcp/server.go, internal/mcp/tools_memory.go
Добавлен параметр ttl_days в schema store_memory; handleStoreMemory принимает необязательный ttl_days, вычисляет/применяет TTL через computeTTLDays и вызывает SetObservationTTL.
API-обработчики
internal/worker/handlers_context.go, internal/worker/handlers_data.go
Аналитика: project стала необязательной (пустой = агрегировать по всем), endpointы принимают/передают параметр since; обработчики используют новый SearchQueryLogStore и возвращают корректные ошибки/пустые ответы при отсутствии хранилища.
UI и клиентские утилиты
ui/src/App.vue, ui/src/components/TimeRangeSelector.vue, ui/src/composables/useSessions.ts, ui/src/utils/api.ts, ui/src/views/AnalyticsView.vue, ui/src/views/GraphView.vue, ui/src/views/PatternsView.vue
Добавлен TimeRangeSelector и интеграция since в вызовы API; обновлены client-side структуры (Pattern.frequency, поля last_seen), сортировки и рендеринг; небольшие UI-правки для скролла и отрисовки графа.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant Worker
    participant SearchStore as SearchQueryLogStore
    participant DB

    Client->>Server: Выполняет поиск (query)
    Server->>Worker: Делегирует запрос поиска
    Worker->>Worker: trackSearchQuery() (в памяти)
    
    rect rgba(100,150,200,0.5)
    Worker->>SearchStore: LogQuery(project, query, type, results, usedVector, latency)
    SearchStore->>DB: INSERT INTO search_query_log ...
    DB-->>SearchStore: OK
    end

    Worker->>Worker: Обновляет in-memory recent buffer
    Worker-->>Client: Возвращает результаты поиска

    Client->>Server: GET /search/analytics?since=...
    Server->>SearchStore: GetAnalytics(since)
    SearchStore->>DB: SELECT aggregate FROM search_query_log WHERE created_at >= since
    DB-->>SearchStore: analytics rows
    SearchStore-->>Server: SearchAnalytics
    Server-->>Client: JSON ответ
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Я записал все запросы в ряд,

в таблицах логов — след и взгляд,
TTL охраняет правду дня,
Временной фильтр — нам помогая,
Пусть аналитика шепчет: «Ура!» 📈

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.25% 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 directly summarizes the main changes: persistent analytics implementation, bug fixes, and TTL support for verified facts.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-analytics-v2
📝 Coding Plan
  • Generate coding plan for human review comments

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 introduces significant enhancements to the dashboard analytics by implementing persistent storage for search queries, which allows for more robust and long-term data analysis. It also greatly improves the user experience with a new time-range selection feature for analytics and critical bug fixes across various dashboard views. Additionally, a new mechanism for managing the lifecycle of 'verified' observations through Time-To-Live (TTL) settings has been implemented, contributing to better data management and relevance.

Highlights

  • Persistent Search Query Logging: Replaced the in-memory ring buffer with a database-backed solution for search analytics, ensuring that search query data persists across server restarts and is available for long-term analysis.
  • Enhanced Analytics Dashboard: Introduced a new TimeRangeSelector component, allowing users to filter analytics data by predefined time periods (Today, 7 days, 30 days, All time), providing more flexible and targeted insights.
  • Verified Facts Time-To-Live (TTL): Implemented Time-To-Live (TTL) support for observations tagged as 'verified', enabling automatic expiration based on configurable rules (e.g., 7 days for API, 30 days for library) or explicit duration, improving data hygiene.
  • UI Fixes and Improvements: Addressed several UI issues across the Patterns, Graph, Analytics, and Sessions pages, including fixing pattern sorting, improving graph rendering stability, and providing more informative error messages for session indexing.
  • Configuration and Documentation Updates: Standardized the authentication token naming (ENGRAM_AUTH_ADMIN_TOKEN) and updated its priority in the configuration, along with corresponding documentation changes.
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

@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 significant enhancements to the dashboard analytics by making them persistent, adds TTL functionality for verified facts, and includes several important bug fixes. The introduction of a persistent search query log is a major improvement over the in-memory buffer. The changes are well-structured, with corresponding database migrations and updates across the backend and frontend. I've identified a couple of areas for improvement: one regarding the robustness of a database query and another related to an incomplete implementation in the new analytics feature.

Comment thread internal/db/gorm/search_query_log_store.go
Comment thread internal/maintenance/service.go Outdated
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/src/composables/useSessions.ts (1)

25-27: ⚠️ Potential issue | 🟠 Major

loading может сбрасываться раньше времени при отменённых конкурентных запросах.

Когда старый запрос прерывается, его finally всё равно делает loading.value = false, даже если новый запрос уже активен. Это приводит к неверному состоянию загрузки (мигание/раннее скрытие индикатора).

Предлагаемая правка через request-id guard
 export function useSessions() {
@@
   let abortController: AbortController | null = null
+  let activeRequestId = 0
@@
   async function loadSessions() {
@@
+    const requestId = ++activeRequestId
@@
     } finally {
-      loading.value = false
+      if (requestId === activeRequestId) {
+        loading.value = false
+      }
     }
   }
@@
   async function search() {
@@
+    const requestId = ++activeRequestId
@@
     } finally {
-      loading.value = false
+      if (requestId === activeRequestId) {
+        loading.value = false
+      }
     }
   }

Also applies to: 41-46, 55-57, 66-70

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

In `@ui/src/composables/useSessions.ts` around lines 25 - 27, Concurrent request
cancellation can cause loading.value to be reset by an earlier request's finally
block; fix by adding a monotonically incremented request id (e.g.,
requestIdCounter and currentRequestId) inside loadSessions (and the analogous
functions at 41-46, 55-57, 66-70) and capture the id in the async call, only
updating state (sessions list and loading.value) if the captured id matches the
latest currentRequestId; increment the counter and set currentRequestId when
starting a request, and in each finally block check the id match before setting
loading.value = false so aborted/older requests cannot flip the loading flag or
overwrite results.
🧹 Nitpick comments (4)
internal/config/config.go (1)

587-590: Уберите дублирующий заголовок комментария над GetWorkerToken().

На Line 587 и Line 588 повторяется описание функции; лучше оставить одну формулировку.

🧹 Предлагаемое упрощение комментария
-// GetWorkerToken returns the worker authentication token.
-// GetWorkerToken returns the admin authentication token.
-// Checks ENGRAM_AUTH_ADMIN_TOKEN first (preferred), falls back to ENGRAM_API_TOKEN (deprecated).
+// GetWorkerToken returns the admin authentication token.
+// Checks ENGRAM_AUTH_ADMIN_TOKEN first (preferred), falls back to ENGRAM_API_TOKEN (deprecated).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/config/config.go` around lines 587 - 590, Удалите дублирующий
заголовок в комментарии над функцией GetWorkerToken: оставьте только одну строку
описания (например "GetWorkerToken returns the worker authentication token.
Checks ENGRAM_AUTH_ADMIN_TOKEN first (preferred), falls back to ENGRAM_API_TOKEN
(deprecated).") и уберите повторяющуюся строку, чтобы комментарий был
однозначным и не содержал двух одинаковых первых строк.
ui/src/views/GraphView.vue (1)

162-168: Рассмотрите использование CSS-классов вместо прямой мутации style.

Проверка на нулевые размеры контейнера — хорошая защита от проблем vis-network. Однако прямая мутация element.style в Vue-компоненте менее идиоматична. Рассмотрите возможность установки CSS-классов или использования привязки :style для более реактивного подхода.

При этом текущая реализация функционально корректна и решает проблему рендеринга графа.

♻️ Альтернативный подход с реактивным стилем

Можно добавить ref для минимальных размеров и использовать его в шаблоне:

<!-- В template -->
<div
  ref="graphContainer"
  :style="containerStyle"
  class="w-full h-full rounded-xl ..."
/>
// В script
const forceMinSize = ref(false)

const containerStyle = computed(() => 
  forceMinSize.value 
    ? { minHeight: '500px', minWidth: '400px' } 
    : {}
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/GraphView.vue` around lines 162 - 168, Replace direct DOM style
mutation with a reactive class/style: introduce a ref forceMinSize and a
computed containerStyle (or a CSS class) that returns { minHeight:'500px',
minWidth:'400px' } when forceMinSize is true; keep the existing zero-size check
using graphContainer (graphContainer.value.getBoundingClientRect()) but set
forceMinSize.value = true instead of mutating graphContainer.value.style; then
bind the computed containerStyle in the template on the same element (use
:style="containerStyle" or a conditional class) so sizing is reactive and
idiomatic in the GraphView component.
internal/db/gorm/models.go (1)

86-87: Рекомендую добавить индекс на expires_at для TTL-мониторинга.

На Line 86 поле ExpiresAt добавлено без индекса, а мониторинг уже считает просроченные записи по expires_at < NOW(). На больших объёмах это станет дорогим full scan.

💡 Предлагаемое изменение
-	ExpiresAt                   sql.NullTime   `gorm:"type:timestamptz"`
+	ExpiresAt                   sql.NullTime   `gorm:"type:timestamptz;index:idx_observations_expires_at"`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/db/gorm/models.go` around lines 86 - 87, The ExpiresAt field in the
GORM model (ExpiresAt sql.NullTime) lacks an index which causes full-table scans
for TTL checks; add an index on ExpiresAt (e.g., by adding a GORM tag like
`gorm:"index:idx_expires_at,sort:asc"` or similar) to the ExpiresAt field in the
model (and if needed create a matching DB migration to create the index) so TTL
queries that use `expires_at < NOW()` can use the index; reference the ExpiresAt
field and update the model and migrations accordingly.
internal/mcp/server.go (1)

1155-1155: Добавьте нижнюю границу для ttl_days в input schema.

На Line 1155 сейчас допускаются 0 и отрицательные значения. Лучше ограничить минимумом 1, чтобы контракт инструмента соответствовал “TTL в днях”.

✅ Небольшое улучшение схемы
-						"ttl_days":   map[string]any{"type": "integer", "description": "TTL in days for verified facts. Auto-computed from tags if not provided. Only applies to observations with 'verified' tag."},
+						"ttl_days":   map[string]any{"type": "integer", "minimum": 1, "description": "TTL in days for verified facts. Auto-computed from tags if not provided. Only applies to observations with 'verified' tag."},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcp/server.go` at line 1155, The schema entry for "ttl_days"
currently allows 0 and negative values; update the input schema map for
"ttl_days" (the map[string]any literal that contains "type" and "description")
to include a minimum constraint so values <1 are rejected — e.g., add "minimum":
1 to that map so the schema enforces TTL in days must be at least 1.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude-plugin/marketplace.json:
- Line 17: Поле "source" содержит неверный путь "./engram"; замените значение
поля "source" на "./plugin/" в манифесте (ищите ключ "source") чтобы указать
правильную директорию плагина для интеграции в маркетплейс и восстановить
установку.

In `@internal/db/gorm/observation_store.go`:
- Around line 1352-1354: The FTS search code paths (SearchObservationsFTS,
SearchObservationsFTSFiltered, SearchObservationsFTSScored and their scanner
scanObservation) are not populating the new TTL fields even though
toModelObservation sets ExpiresAt, TtlDays and IsExpired; update the SELECTs
used by these FTS queries to include the ExpiresAt and TtlDays columns and
modify scanObservation (and any row->struct scan helpers used by the three FTS
functions) to read and populate ExpiresAt, TtlDays and compute/set IsExpired
consistently (e.g., using ExpiresAt.Valid && ExpiresAt.Time.Before(time.Now()))
so FTS results include the expiration metadata.

In `@internal/db/gorm/search_query_log_store.go`:
- Around line 37-53: The current LogQuery method in SearchQueryLogStore spawns
an unbounded fire-and-forget goroutine per call (creating SearchQueryLogEntry
and calling s.db.Create), which can overwhelm the process under load; replace
this by implementing a bounded background worker or batcher: add a buffered
channel on SearchQueryLogStore to enqueue SearchQueryLogEntry, start a fixed
worker goroutine (or a small pool) that reads from the channel, performs batched
writes using GORM's CreateInBatches or transactions (or retries on error), and
add shutdown tracking (a Close/Shutdown method and context/cancel to drain the
queue) so LogQuery only enqueues (non-blocking with a drop/metrics strategy when
full) instead of spawning goroutines directly; update uses of LogQuery to create
entries and push them to the new channel and remove the s.db.Create call from
LogQuery itself.
- Around line 90-121: The code currently ignores DB errors on each aggregate
which can produce mixed real/zero analytics; update the blocks that call
s.db.WithContext(...).Table("search_query_log") so you capture and check the
GORM result errors and Scan errors (e.g., assign result :=
s.db.WithContext(ctx)...Count(&analytics.SearchesToday); if result.Error != nil
{ return err } and similarly for the AVG query where
Row().Scan(&analytics.AvgLatencyMs) should check the returned error, for
zeroCount use result := zeroQ.Where(...).Count(&zeroCount); if result.Error !=
nil { return err }, and for vectorQ use result :=
vectorQ.Where(...).Count(&analytics.VectorSearches); if result.Error != nil {
return err }; on any error or cancelled context return the error rather than
returning partially filled analytics (refer to symbols: SearchesToday,
AvgLatencyMs, ZeroResultRate/zeroCount, VectorSearches, TotalSearches,
s.db.WithContext, Row().Scan).
- Around line 50-52: Не логируйте сырой пользовательский ввод из переменной
query в warning: вместо этого при ошибке вставки (в месте вызова
s.db.Create(&entry).Error) замените логирование полного запроса на безопасные
метаданные — например log.Warn().Err(err).Int("query_len",
len(query)).Str("query_hash", <sha256_hex_of_query>).Msg("failed to log search
query"); реализуйте хеширование (sha256) и длину рядом с существующим
сообщением, чтобы сохранить контекст для отладки без утечки PII/секретов, при
этом не меняя сохранение самого entry в БД.

In `@internal/maintenance/service.go`:
- Around line 214-216: The LIKE filter "concepts::text LIKE '%verified%'" in the
query that ends with Count(&expiredCount).Error is imprecise and causes
full-table scans; replace it with a JSONB containment check to match the exact
tag (for example use Where("concepts @> ?::jsonb", `["verified"]`) or equivalent
JSONB operator used in your codebase) so only rows whose concepts array contains
"verified" are counted, and add the suggested partial index migration (CREATE
INDEX CONCURRENTLY ... WHERE expires_at IS NOT NULL) to improve performance.

In `@internal/mcp/tools_memory.go`:
- Around line 188-205: The handler currently calls
s.observationStore.SetObservationTTL(...) after creating the observation but
still returns success and includes "ttl_days" in the result even when
SetObservationTTL fails; change the logic in the function handling observation
creation (where computeTTLDays, ttlDays and s.observationStore.SetObservationTTL
are used) so that on SetObservationTTL error you do not advertise the TTL:
either return an error response (fail the request) or at minimum omit "ttl_days"
from the result when SetObservationTTL returns an error; for a stronger fix
consider making creation + TTL update atomic (wrap both actions into a
transaction or a single store call) so the observation is only reported as
successful if the TTL was persisted.
- Around line 213-247: computeTTLDays currently returns the first matching
autoTTL entry (dependent on concepts order) which makes TTL non-deterministic;
change the logic in computeTTLDays to, after verifying the "verified" tag, scan
all concepts and collect all matching autoTTL values from the autoTTL map
(variable autoTTL) and return the minimum matched days (instead of returning on
first match); keep the explicit override check (explicit and *explicit > 0) and
the default return 30 for verified facts with no matching tag.

In `@internal/worker/service.go`:
- Around line 1918-1928: trackSearchQuery currently hardcodes latency_ms = 0
which corrupts analytics; instead propagate the real measured latency (or NULL
when unavailable) into the DB write. Update call sites to compute elapsed time
for searches and pass that value into trackSearchQuery, and change
trackSearchQuery to forward that measured latency to
s.searchQueryLogStore.LogQuery (replace the trailing 0 argument with the actual
latency or a nil/nullable sentinel supported by LogQuery). Ensure LogQuery’s
parameter contract is respected (make it accept a nullable/optional latency if
needed) and preserve the existing fire-and-forget behavior using the same
s.initMu and s.searchQueryLogStore symbols.
- Around line 896-897: The searchQueryLogStore is only created in
initializeAsync() and thus remains tied to the old store after
reinitializeDatabase(); update reinitializeDatabase() to (re)create and assign a
new searchQueryLogStore using gorm.NewSearchQueryLogStore(store.GetDB()) (same
way as in initializeAsync()), so handlers/functions like trackSearchQuery and
the /api/search/recent and /api/search/analytics endpoints use the fresh
store/DB after reinitialization; ensure any existing reference to the old
searchQueryLogStore (variable name: searchQueryLogStore) is replaced/updated
when the new store is created.

In `@pkg/models/observation.go`:
- Around line 219-221: Observation's new fields ExpiresAt, TtlDays and IsExpired
aren't emitted because MarshalJSON builds an ObservationJSON without those
fields; update the JSON path by adding ExpiresAt (sql.NullTime -> appropriate
nullable/time value), TtlDays (sql.NullInt32 -> int/null) and IsExpired (bool)
to the ObservationJSON type used in MarshalJSON or adjust MarshalJSON to copy
those fields from Observation into the output map/struct, preserving the json
tags `expires_at`, `ttl_days`, `is_expired` and handling NullTime/NullInt32
conversion to nullable JSON values.

In `@README.md`:
- Line 124: Update README examples to use the new environment variable name
consistently: replace occurrences of ENGRAM_API_TOKEN with
ENGRAM_AUTH_ADMIN_TOKEN in the Authorization header examples, CLI command
examples, and any env var snippets; keep a short note that ENGRAM_API_TOKEN is
deprecated (if present) and accept both env vars in examples only if you show
precedence or fallback, but do not leave mixed references that could break users
who set only ENGRAM_AUTH_ADMIN_TOKEN.

In `@ui/src/components/TimeRangeSelector.vue`:
- Around line 26-29: The comment is wrong because start.toISOString() returns
UTC (with Z) not an ISO string with local offset; update the 'today' branch to
serialize the local-start Date with its timezone offset instead of using
toISOString(): create a helper (e.g., toISOWithOffset(date)) that computes the
timezone offset, formats ±HH:MM, and appends it to the ISO datetime, then return
toISOWithOffset(start) from the case 'today' branch (reference the start
variable and the toISOWithOffset helper).
- Around line 4-6: The prop modelValue is defined but not used to initialize or
sync the internal selected state; update the component (TimeRangeSelector.vue)
to initialize selected from props.modelValue (when present), add a watcher on
modelValue to update selected when parent changes it, and ensure selected
changes emit the v-model update via emit('update:modelValue', selected) (use the
existing props/modelValue and the component's selected reactive ref and
defineEmits).

In `@ui/src/composables/useSessions.ts`:
- Around line 5-11: The check in resolveSessionError currently uses
err.message.startsWith('HTTP 503'); change it to use a substring check like
err.message.includes('503') (or includes('HTTP 503')) for consistency with other
API error checks; update the conditional in resolveSessionError so it detects
HTTP 503 via .includes('503') and otherwise returns err.message || fallback.

In `@ui/src/views/AnalyticsView.vue`:
- Around line 57-63: The component currently calls loadAll() twice on mount
because onMounted calls loadAll() and the TimeRangeSelector emits
update:modelValue with immediate:true which triggers watch(since) → loadAll();
to fix, remove the onMounted(() => loadAll()) call OR add an initialized flag
(e.g., isInitialized) checked/set inside the watch(since, ...) handler so the
first immediate watch invocation runs once and subsequent changes call
loadAll(); update references to abortController?.abort() remain as-is to cancel
in-flight requests.

---

Outside diff comments:
In `@ui/src/composables/useSessions.ts`:
- Around line 25-27: Concurrent request cancellation can cause loading.value to
be reset by an earlier request's finally block; fix by adding a monotonically
incremented request id (e.g., requestIdCounter and currentRequestId) inside
loadSessions (and the analogous functions at 41-46, 55-57, 66-70) and capture
the id in the async call, only updating state (sessions list and loading.value)
if the captured id matches the latest currentRequestId; increment the counter
and set currentRequestId when starting a request, and in each finally block
check the id match before setting loading.value = false so aborted/older
requests cannot flip the loading flag or overwrite results.

---

Nitpick comments:
In `@internal/config/config.go`:
- Around line 587-590: Удалите дублирующий заголовок в комментарии над функцией
GetWorkerToken: оставьте только одну строку описания (например "GetWorkerToken
returns the worker authentication token. Checks ENGRAM_AUTH_ADMIN_TOKEN first
(preferred), falls back to ENGRAM_API_TOKEN (deprecated).") и уберите
повторяющуюся строку, чтобы комментарий был однозначным и не содержал двух
одинаковых первых строк.

In `@internal/db/gorm/models.go`:
- Around line 86-87: The ExpiresAt field in the GORM model (ExpiresAt
sql.NullTime) lacks an index which causes full-table scans for TTL checks; add
an index on ExpiresAt (e.g., by adding a GORM tag like
`gorm:"index:idx_expires_at,sort:asc"` or similar) to the ExpiresAt field in the
model (and if needed create a matching DB migration to create the index) so TTL
queries that use `expires_at < NOW()` can use the index; reference the ExpiresAt
field and update the model and migrations accordingly.

In `@internal/mcp/server.go`:
- Line 1155: The schema entry for "ttl_days" currently allows 0 and negative
values; update the input schema map for "ttl_days" (the map[string]any literal
that contains "type" and "description") to include a minimum constraint so
values <1 are rejected — e.g., add "minimum": 1 to that map so the schema
enforces TTL in days must be at least 1.

In `@ui/src/views/GraphView.vue`:
- Around line 162-168: Replace direct DOM style mutation with a reactive
class/style: introduce a ref forceMinSize and a computed containerStyle (or a
CSS class) that returns { minHeight:'500px', minWidth:'400px' } when
forceMinSize is true; keep the existing zero-size check using graphContainer
(graphContainer.value.getBoundingClientRect()) but set forceMinSize.value = true
instead of mutating graphContainer.value.style; then bind the computed
containerStyle in the template on the same element (use :style="containerStyle"
or a conditional class) so sizing is reactive and idiomatic in the GraphView
component.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9cf8a562-cf47-4b7a-9ffd-ff1c91ff31a7

📥 Commits

Reviewing files that changed from the base of the PR and between 6d98b7c and c6c15f4.

📒 Files selected for processing (22)
  • .claude-plugin/marketplace.json
  • README.md
  • internal/config/config.go
  • internal/db/gorm/migrations.go
  • internal/db/gorm/models.go
  • internal/db/gorm/observation_store.go
  • internal/db/gorm/search_query_log_store.go
  • internal/maintenance/service.go
  • internal/mcp/server.go
  • internal/mcp/tools_memory.go
  • internal/worker/handlers_context.go
  • internal/worker/handlers_data.go
  • internal/worker/handlers_maintenance.go
  • internal/worker/service.go
  • pkg/models/observation.go
  • ui/src/App.vue
  • ui/src/components/TimeRangeSelector.vue
  • ui/src/composables/useSessions.ts
  • ui/src/utils/api.ts
  • ui/src/views/AnalyticsView.vue
  • ui/src/views/GraphView.vue
  • ui/src/views/PatternsView.vue

Comment thread .claude-plugin/marketplace.json Outdated
Comment thread internal/db/gorm/observation_store.go
Comment thread internal/db/gorm/search_query_log_store.go
Comment thread internal/db/gorm/search_query_log_store.go
Comment thread internal/db/gorm/search_query_log_store.go Outdated
Comment thread README.md
Comment thread ui/src/components/TimeRangeSelector.vue Outdated
Comment thread ui/src/components/TimeRangeSelector.vue
Comment thread ui/src/composables/useSessions.ts
Comment thread ui/src/views/AnalyticsView.vue Outdated
@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 20, 2026

@codex review

…rors, security

- Fix ObservationJSON/MarshalJSON missing TTL fields (expires_at, ttl_days, is_expired)
- Add TTL columns to FTS raw SQL queries and scanObservation
- Fix computeTTLDays to use minimum TTL from all matching tags (not first match)
- Only confirm TTL in response if SetObservationTTL succeeded
- Fix search_query_log_store: check errors on all aggregates, remove raw query from logs
- Add real latency measurement to trackSearchQuery callers
- Add searchQueryLogStore to reinitializeDatabase path
- Fix marketplace.json source path regression (./engram -> ./plugin/engram)
- Sync README token variable to ENGRAM_AUTH_ADMIN_TOKEN
- Use JSONB containment operator for verified facts filter in maintenance
- Add minimum:1 to ttl_days MCP schema
- Fix double loadAll() in AnalyticsView, fix misleading comment in TimeRangeSelector
@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 20, 2026

🤖 PR Review MCP State (auto-managed, do not edit)
{
  "version": 2,
  "parentChildren": {},
  "resolvedNitpicks": {
    "coderabbit-nitpick-5aa2c832-1155": {
      "resolvedAt": "2026-03-20T00:46:46.062Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-21cd21d5-86": {
      "resolvedAt": "2026-03-20T00:46:48.501Z",
      "resolvedBy": "agent"
    },
    "coderabbit-outside-diff-155be32f-587": {
      "resolvedAt": "2026-03-20T00:47:02.104Z",
      "resolvedBy": "agent"
    },
    "coderabbit-outside-diff-650755dc-162": {
      "resolvedAt": "2026-03-20T00:47:04.789Z",
      "resolvedBy": "agent"
    },
    "coderabbit-outside-diff-21cd21d5-86": {
      "resolvedAt": "2026-03-20T00:47:07.301Z",
      "resolvedBy": "agent"
    },
    "coderabbit-outside-diff-5aa2c832-1155": {
      "resolvedAt": "2026-03-20T00:47:09.809Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-f61b83eb-587": {
      "resolvedAt": "2026-03-20T00:47:12.040Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-650755dc-162": {
      "resolvedAt": "2026-03-20T00:47:14.812Z",
      "resolvedBy": "agent"
    }
  },
  "updatedAt": "2026-03-20T00:47:15.527Z"
}

@thebtf thebtf merged commit 00d4a38 into main Mar 20, 2026
2 checks passed
@thebtf thebtf deleted the feat/dashboard-analytics-v2 branch March 20, 2026 00:48
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