diff --git a/docs/frontend-auth.md b/docs/frontend-auth.md index 30bc764..ff60186 100644 --- a/docs/frontend-auth.md +++ b/docs/frontend-auth.md @@ -61,7 +61,7 @@ The `/dashboard` page reads from the metrics endpoints described in [metrics.md] **Default range:** last 7 days, inclusive of today. Computed at mount in UTC. There's no date-range picker yet. -**Refetch behavior:** mount + `visibilitychange` (when the tab returns to the foreground). A 1-second debounce skips redundant refetches when the visibility flips rapidly. **No polling timer** — the ETL is server-side, refreshes every ~5 minutes, and the dashboard's job is to ask for the latest only when the user is actually looking. +**Refetch behavior:** mount + `visibilitychange` (when the tab returns to the foreground, debounced 1s) + a 60-second polling timer that calls `refetch()` on every card to stay in lockstep with the backend ETL cadence (`METRICS_ETL_INTERVAL_SECONDS`, configured to 60s in production). Pre-2026-05 builds had no timer; this was added when the ETL was tightened from 300s to 60s for live-feel demos. **Card states.** Each metric card renders one of four states: - `loading` — skeleton placeholder diff --git a/docs/metrics.md b/docs/metrics.md index 2052dbf..60a3e3b 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -48,7 +48,7 @@ curl -s "http://localhost:3000/api/v1/metrics/context-switching?from=2026-05-01& ## ETL — how the aggregates are produced -The periodic job [src/services/metrics-etl-scheduler.js](../src/services/metrics-etl-scheduler.js) ticks every `METRICS_ETL_INTERVAL_SECONDS` (default 300). On each tick: +The periodic job [src/services/metrics-etl-scheduler.js](../src/services/metrics-etl-scheduler.js) ticks every `METRICS_ETL_INTERVAL_SECONDS` (code default 300, production runs at 60 — the dashboard's refresh timer is matched to this). On each tick: 1. Reads up to `METRICS_ETL_BATCH_SIZE` (default 5000) new `activities` rows since the watermark stored in `etl_jobs.last_processed_activity_id`. 2. Filters to `text_change` and `editor_switch` events; ignores all others (but still advances the watermark past them). @@ -83,7 +83,7 @@ This uses `sequelize.sync()` (no `force: true`) so it will NOT touch existing ta | Variable | Default | Purpose | | --- | --- | --- | -| `METRICS_ETL_INTERVAL_SECONDS` | `300` | Seconds between scheduler ticks. | +| `METRICS_ETL_INTERVAL_SECONDS` | `300` | Seconds between scheduler ticks. Set to `60` in the cluster Secret so the dashboard's 60s auto-refresh sees fresh aggregates. | | `METRICS_ETL_BATCH_SIZE` | `5000` | Max `activities` rows consumed per pass. | | `METRICS_ETL_ENABLED` | `true` | Set to `false` or `0` to disable the scheduler (useful for tests / local debugging). | | `ADMIN_USER_IDS` | _(empty)_ | Comma-separated UUIDs allowed to call `POST /metrics/etl/run`. | diff --git a/src/services/insight-trigger.service.js b/src/services/insight-trigger.service.js index 9145cde..8211cdc 100644 --- a/src/services/insight-trigger.service.js +++ b/src/services/insight-trigger.service.js @@ -135,8 +135,11 @@ function evaluateRules({ metrics, session }) { if (veryLongSession) { return { triggered: true, rule: 'very_long_session' }; } - if (longSession && highChurn) { - return { triggered: true, rule: 'long_session_high_churn' }; + if (highChurn) { + return { triggered: true, rule: 'high_churn' }; + } + if (longSession) { + return { triggered: true, rule: 'long_session' }; } if (rapidSwitching) { return { triggered: true, rule: 'rapid_context_switching' }; @@ -225,7 +228,23 @@ export async function evaluateUser(userId) { const metrics = await getTodayMetrics(userId); const rule = evaluateRules({ metrics, session }); if (!rule.triggered) { - return { skipped: true, reason: 'no_rule_fired' }; + const diagnostics = { + duration_minutes: session?.duration_minutes ?? 0, + lines_added: metrics.lines_added, + lines_deleted: metrics.lines_deleted, + churn_ratio: metrics.churn_ratio, + switch_count: metrics.switch_count, + rapid_switch_count: metrics.rapid_switch_count, + thresholds: { + very_long_session_min: VERY_LONG_SESSION_MIN(), + long_session_min: LONG_SESSION_MIN(), + high_churn_ratio: HIGH_CHURN_RATIO(), + rapid_switch_count: RAPID_SWITCH_COUNT(), + delete_heavy_total: DELETE_HEAVY_TOTAL(), + }, + }; + logger.info('insight-trigger: no rule fired', { user_id: userId, ...diagnostics }); + return { skipped: true, reason: 'no_rule_fired', diagnostics }; } let llmOutput;