Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/frontend-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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`. |
Expand Down
25 changes: 22 additions & 3 deletions src/services/insight-trigger.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
Expand Down Expand Up @@ -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;
Expand Down
Loading