codexmeter 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server/ingest.js CHANGED
@@ -9,17 +9,14 @@ import { buildAggregates, buildSessionView } from './aggregator.js';
9
9
  import { createDayKeyFormatter } from './day-key.js';
10
10
  import { createLiveAggregateState, createEmptyLivePatch, applySessionToLiveState, buildLiveBootstrap, buildLivePatch } from './live-state.js';
11
11
  import { createRolloutWorkerPool } from './rollout-worker-pool.js';
12
+ import { beginReplayCapture, createReplayCaptureState, failReplayCapture, getReplaySnapshot, recordReplayEvent, resetReplayCapture } from './export-replay.js';
13
+ import { OVERVIEW_INGEST_ANIMATION } from '../src/utils/animationsDefault.js';
12
14
 
13
15
  const LIVE_FRAME_INTERVAL_MS = 50;
14
16
  const LIVE_DAYS_PER_SECOND = 6;
15
- const LIVE_DAY_CADENCE_MS = Math.round(1000 / LIVE_DAYS_PER_SECOND);
17
+ const LIVE_OVERVIEW_CADENCE_MS = Math.round(1000 / 10);
16
18
  const LIVE_DAY_KEYS_PER_EMIT = 1;
17
- const LIVE_SURFACE_CADENCE_MS = {
18
- overview: LIVE_FRAME_INTERVAL_MS * 2,
19
- rankings: LIVE_FRAME_INTERVAL_MS * 3,
20
- daily: LIVE_DAY_CADENCE_MS,
21
- heatmap: LIVE_DAY_CADENCE_MS,
22
- };
19
+ const LIVE_SESSION_REORDER_BUFFER = 160;
23
20
 
24
21
  export function createIngestState() {
25
22
  return {
@@ -43,9 +40,11 @@ export function createIngestState() {
43
40
  live_subscribers: new Set(),
44
41
  live_pump_timer: null,
45
42
  live_pending_patch: createEmptyLivePatch(),
43
+ live_session_buffer: [],
46
44
  live_progress_dirty: false,
47
45
  live_last_emit_at: 0,
48
- live_last_surface_emit_at: { overview: 0, rankings: 0, daily: 0, heatmap: 0 },
46
+ live_last_overview_emit_at: 0,
47
+ replay_capture: createReplayCaptureState(),
49
48
  };
50
49
  }
51
50
 
@@ -60,6 +59,12 @@ export async function runIngest(codexHome, state, opts = {}) {
60
59
  state.live_state = createLiveAggregateState(tz);
61
60
  state.phase = 'inventory';
62
61
  state.percent = 0;
62
+ beginReplayCapture(state.replay_capture, state.ingest_id, {
63
+ ingest_id: state.ingest_id,
64
+ seq: 0,
65
+ progress: progressPayload(state),
66
+ data: buildLiveBootstrap(state.live_state),
67
+ });
63
68
  queueLiveProgress(state);
64
69
  broadcastBootstrap(state);
65
70
 
@@ -100,6 +105,7 @@ export async function runIngest(codexHome, state, opts = {}) {
100
105
  usage_total: null,
101
106
  usage_by_day: null,
102
107
  has_usage_by_day: false,
108
+ live_sort_ts: null,
103
109
  active_by_day: null,
104
110
  agent_role: t.agent_role,
105
111
  agent_nickname: t.agent_nickname,
@@ -134,7 +140,7 @@ export async function runIngest(codexHome, state, opts = {}) {
134
140
 
135
141
  state.needs_enrichment = candidates.length;
136
142
  state.percent = candidates.length > 0 ? 0.08 : 0.90;
137
- const BATCH_SIZE = opts.batchSize || 100;
143
+ const BATCH_SIZE = opts.batchSize || 40;
138
144
  const ROOT_REFRESH_EVERY = opts.rootRefreshEvery || 1000;
139
145
  let lastRootRefreshCount = 0;
140
146
 
@@ -166,12 +172,16 @@ export async function runIngest(codexHome, state, opts = {}) {
166
172
  s.usage_by_day = buildUsageByDayMetrics(s.model_name, data.usage_by_day);
167
173
  s.has_usage_by_day = s.usage_by_day.length > 0;
168
174
  }
175
+ if (data.first_usage_timestamp) {
176
+ s.live_sort_ts = Math.floor(data.first_usage_timestamp / 1000);
177
+ }
169
178
  if (data.active_seconds && data.active_seconds > 0) {
170
179
  s.elapsed_seconds = data.active_seconds;
171
180
  s.active_by_day = data.active_by_day || null;
172
181
  }
173
182
  }
174
183
  finalizeSessionMetrics(s, toDayKey);
184
+ s.live_sort_day = deriveLiveSortDay(s, toDayKey);
175
185
  s.materialized = true;
176
186
  }
177
187
 
@@ -183,8 +193,14 @@ export async function runIngest(codexHome, state, opts = {}) {
183
193
  assignRootThreadIds(sessions);
184
194
  lastRootRefreshCount = state.enriched;
185
195
  }
196
+ const liveReady = bufferLiveSessionsForPresentation(state, batch);
197
+ if (state.live_session_buffer[0]?.live_sort_day) {
198
+ state.current_date_bucket = state.live_session_buffer[0].live_sort_day;
199
+ } else if (liveReady[0]?.live_sort_day) {
200
+ state.current_date_bucket = liveReady[0].live_sort_day;
201
+ }
186
202
  const livePatch = createEmptyLivePatch();
187
- for (const session of batch) {
203
+ for (const session of liveReady) {
188
204
  applySessionToLiveState(state.live_state, session, livePatch);
189
205
  }
190
206
  queueLivePatch(state, livePatch);
@@ -203,6 +219,11 @@ export async function runIngest(codexHome, state, opts = {}) {
203
219
  }
204
220
 
205
221
  assignRootThreadIds(sessions);
222
+ const finalLivePatch = createEmptyLivePatch();
223
+ for (const session of flushBufferedLiveSessions(state)) {
224
+ applySessionToLiveState(state.live_state, session, finalLivePatch);
225
+ }
226
+ queueLivePatch(state, finalLivePatch);
206
227
 
207
228
  state.phase = 'aggregation';
208
229
  state.percent = state.needs_enrichment > 0 ? 0.95 : 0.90;
@@ -222,6 +243,7 @@ export async function runIngest(codexHome, state, opts = {}) {
222
243
  if (!isCurrentRun()) return;
223
244
  state.error = err.message;
224
245
  state.phase = 'error';
246
+ failReplayCapture(state.replay_capture);
225
247
  console.error('Ingest error:', err);
226
248
  flushLive(state, 'ingest-error');
227
249
  } finally {
@@ -253,9 +275,11 @@ export function restartIngest(codexHome, state, opts = {}) {
253
275
  state.live_state = null;
254
276
  state.live_seq = 0;
255
277
  state.live_pending_patch = createEmptyLivePatch();
278
+ state.live_session_buffer = [];
256
279
  state.live_progress_dirty = false;
257
280
  state.live_last_emit_at = 0;
258
- state.live_last_surface_emit_at = { overview: 0, rankings: 0, daily: 0, heatmap: 0 };
281
+ state.live_last_overview_emit_at = 0;
282
+ resetReplayCapture(state.replay_capture);
259
283
 
260
284
  return runIngest(codexHome, state, opts);
261
285
  }
@@ -373,6 +397,49 @@ function buildUsageByDayMetrics(modelName, usageByDay) {
373
397
  return entries;
374
398
  }
375
399
 
400
+ function deriveLiveSortDay(session, toDayKey) {
401
+ if (session.has_usage_by_day && session.usage_by_day?.length) {
402
+ let best = session.usage_by_day[0];
403
+ for (const entry of session.usage_by_day) {
404
+ if ((entry.tokens || 0) > (best.tokens || 0)) {
405
+ best = entry;
406
+ continue;
407
+ }
408
+ if ((entry.tokens || 0) === (best.tokens || 0) && String(entry.day) > String(best.day)) {
409
+ best = entry;
410
+ }
411
+ }
412
+ return best?.day || null;
413
+ }
414
+ if (session.started_at) {
415
+ return toDayKey(session.started_at * 1000);
416
+ }
417
+ return null;
418
+ }
419
+
420
+ function compareLiveSessionOrder(a, b) {
421
+ const dayCompare = String(a.live_sort_day || '9999-12-31').localeCompare(String(b.live_sort_day || '9999-12-31'));
422
+ if (dayCompare !== 0) return dayCompare;
423
+ const liveTsCompare = (a.live_sort_ts || a.started_at || 0) - (b.live_sort_ts || b.started_at || 0);
424
+ if (liveTsCompare !== 0) return liveTsCompare;
425
+ return String(a.thread_id || '').localeCompare(String(b.thread_id || ''));
426
+ }
427
+
428
+ function bufferLiveSessionsForPresentation(state, sessions) {
429
+ if (!sessions.length) return [];
430
+ state.live_session_buffer.push(...sessions);
431
+ state.live_session_buffer.sort(compareLiveSessionOrder);
432
+ const flushCount = Math.max(0, state.live_session_buffer.length - LIVE_SESSION_REORDER_BUFFER);
433
+ if (flushCount <= 0) return [];
434
+ return state.live_session_buffer.splice(0, flushCount);
435
+ }
436
+
437
+ function flushBufferedLiveSessions(state) {
438
+ if (!state.live_session_buffer.length) return [];
439
+ state.live_session_buffer.sort(compareLiveSessionOrder);
440
+ return state.live_session_buffer.splice(0, state.live_session_buffer.length);
441
+ }
442
+
376
443
  function queueLiveProgress(state) {
377
444
  state.live_progress_dirty = true;
378
445
  ensureLivePump(state);
@@ -385,13 +452,14 @@ function queueLivePatch(state, patch) {
385
452
  }
386
453
 
387
454
  function ensureLivePump(state) {
388
- if (state.live_pump_timer || !state.live_subscribers.size) return;
455
+ if (state.live_pump_timer) return;
456
+ if (!state.live_subscribers.size && !state.replay_capture.active) return;
389
457
  state.live_pump_timer = setInterval(() => flushLive(state), LIVE_FRAME_INTERVAL_MS);
390
458
  }
391
459
 
392
460
  function stopLivePumpIfIdle(state) {
393
461
  if (!state.live_pump_timer) return;
394
- if (state.live_subscribers.size) return;
462
+ if (state.live_subscribers.size || state.replay_capture.active) return;
395
463
  clearInterval(state.live_pump_timer);
396
464
  state.live_pump_timer = null;
397
465
  finalizeWithoutSubscribers(state);
@@ -399,6 +467,7 @@ function stopLivePumpIfIdle(state) {
399
467
 
400
468
  function finalizeWithoutSubscribers(state) {
401
469
  if (state.live_subscribers.size) return;
470
+ if (state.replay_capture.active) return;
402
471
  if (!state.presentation_complete_pending) return;
403
472
  state.phase = 'complete';
404
473
  state.percent = 1;
@@ -409,7 +478,7 @@ function finalizeWithoutSubscribers(state) {
409
478
  }
410
479
 
411
480
  function flushLive(state, forcedEvent = null) {
412
- if (!state.live_subscribers.size) {
481
+ if (!state.live_subscribers.size && !state.replay_capture.active) {
413
482
  state.live_pending_patch = createEmptyLivePatch();
414
483
  state.live_progress_dirty = false;
415
484
  stopLivePumpIfIdle(state);
@@ -449,12 +518,16 @@ function flushLive(state, forcedEvent = null) {
449
518
  payload.data = buildLiveBootstrap(state.live_state);
450
519
  }
451
520
 
452
- broadcastLive(state, event, payload);
521
+ recordReplayEvent(state.replay_capture, event, payload);
522
+ if (state.live_subscribers.size) {
523
+ broadcastLive(state, event, payload);
524
+ }
453
525
  state.live_last_emit_at = Date.now();
454
526
  state.live_progress_dirty = shouldEmitComplete ? false : (event === 'progress' ? false : state.live_progress_dirty);
455
527
  if (forcedEvent) {
456
528
  state.live_pending_patch = createEmptyLivePatch();
457
529
  }
530
+ stopLivePumpIfIdle(state);
458
531
  }
459
532
 
460
533
  function broadcastLive(state, event, payload) {
@@ -496,41 +569,48 @@ function takeFlushablePatch(state) {
496
569
  const now = Date.now();
497
570
  const sent = createEmptyLivePatch();
498
571
 
499
- if (state.live_pending_patch.overview.size > 0 && readyForSurface(state, 'overview', now)) {
500
- moveSet(state.live_pending_patch.overview, sent.overview);
501
- state.live_last_surface_emit_at.overview = now;
502
- }
503
-
504
- const rankingsDirty =
572
+ const overviewDirty =
573
+ state.live_pending_patch.overview.size > 0 ||
505
574
  state.live_pending_patch.repos.total.size > 0 || state.live_pending_patch.repos.d7.size > 0 || state.live_pending_patch.repos.d30.size > 0 ||
506
575
  state.live_pending_patch.models.total.size > 0 || state.live_pending_patch.models.d7.size > 0 || state.live_pending_patch.models.d30.size > 0 ||
507
- state.live_pending_patch.families.total.size > 0 || state.live_pending_patch.families.d7.size > 0 || state.live_pending_patch.families.d30.size > 0;
576
+ state.live_pending_patch.families.total.size > 0 || state.live_pending_patch.families.d7.size > 0 || state.live_pending_patch.families.d30.size > 0 ||
577
+ state.live_pending_patch.daily.size > 0 || state.live_pending_patch.heatmap.size > 0;
508
578
 
509
- if (rankingsDirty && readyForSurface(state, 'rankings', now)) {
510
- moveRangeSets(state.live_pending_patch.repos, sent.repos);
511
- moveRangeSets(state.live_pending_patch.models, sent.models);
512
- moveRangeSets(state.live_pending_patch.families, sent.families);
513
- state.live_last_surface_emit_at.rankings = now;
579
+ if (!overviewDirty || !readyForOverview(state, now)) {
580
+ return sent;
514
581
  }
515
582
 
516
- const dayDirty = state.live_pending_patch.daily.size > 0 || state.live_pending_patch.heatmap.size > 0;
517
- if (dayDirty && readyForSurface(state, 'daily', now)) {
518
- const nextDayKeys = takeNextChronologicalDayKeys(
519
- state.live_pending_patch.daily,
520
- state.live_pending_patch.heatmap,
521
- LIVE_DAY_KEYS_PER_EMIT
522
- );
523
- moveSpecificKeys(state.live_pending_patch.daily, sent.daily, nextDayKeys);
524
- moveSpecificKeys(state.live_pending_patch.heatmap, sent.heatmap, nextDayKeys);
525
- state.live_last_surface_emit_at.daily = now;
526
- state.live_last_surface_emit_at.heatmap = now;
527
- }
583
+ moveSet(state.live_pending_patch.overview, sent.overview);
584
+ moveRangeSets(state.live_pending_patch.repos, sent.repos);
585
+ moveRangeSets(state.live_pending_patch.models, sent.models);
586
+ moveRangeSets(state.live_pending_patch.families, sent.families);
587
+
588
+ const nextDayKeys = takeNextChronologicalDayKeys(
589
+ state.live_pending_patch.daily,
590
+ state.live_pending_patch.heatmap,
591
+ LIVE_DAY_KEYS_PER_EMIT
592
+ );
593
+ moveSpecificKeys(state.live_pending_patch.daily, sent.daily, nextDayKeys);
594
+ moveSpecificKeys(state.live_pending_patch.heatmap, sent.heatmap, nextDayKeys);
595
+ state.live_last_overview_emit_at = now;
528
596
 
529
597
  return sent;
530
598
  }
531
599
 
532
- function readyForSurface(state, surfaceKey, now) {
533
- return (now - state.live_last_surface_emit_at[surfaceKey]) >= LIVE_SURFACE_CADENCE_MS[surfaceKey];
600
+ function readyForOverview(state, now) {
601
+ return (now - state.live_last_overview_emit_at) >= getOverviewCadenceMs(state);
602
+ }
603
+
604
+ function getOverviewCadenceMs(state) {
605
+ const tail = OVERVIEW_INGEST_ANIMATION.tail;
606
+ const tailStartPercent = Math.min(Math.max(tail?.startPercent ?? 0.95, 0), 0.999);
607
+ const tailHz = Number.isFinite(tail?.overviewHz) && tail.overviewHz > 0 ? tail.overviewHz : 5;
608
+ const tailCadenceMs = Math.round(1000 / tailHz);
609
+
610
+ if (tail?.enabled && (state.presentation_complete_pending || (state.percent || 0) >= tailStartPercent)) {
611
+ return tailCadenceMs;
612
+ }
613
+ return LIVE_OVERVIEW_CADENCE_MS;
534
614
  }
535
615
 
536
616
  function moveSet(from, to) {
@@ -602,6 +682,10 @@ export function detachLiveSubscriber(state, res) {
602
682
  stopLivePumpIfIdle(state);
603
683
  }
604
684
 
685
+ export function getLatestReplay(state) {
686
+ return getReplaySnapshot(state.replay_capture);
687
+ }
688
+
605
689
  function broadcastBootstrap(state) {
606
690
  if (!state.live_subscribers.size) return;
607
691
  const payload = {
@@ -15,6 +15,7 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
15
15
  model_name: null,
16
16
  reasoning_effort: null,
17
17
  first_timestamp: null,
18
+ first_usage_timestamp: null,
18
19
  last_timestamp: null,
19
20
  active_seconds: null,
20
21
  active_by_day: null,
@@ -97,6 +98,9 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
97
98
  if (obj.timestamp && hasUsage(usageDelta)) {
98
99
  const ts = new Date(obj.timestamp).getTime();
99
100
  if (!isNaN(ts)) {
101
+ if (!result.first_usage_timestamp || ts < result.first_usage_timestamp) {
102
+ result.first_usage_timestamp = ts;
103
+ }
100
104
  const dayKey = toDayKey(ts);
101
105
  mergeUsageTotals(usageByDay, dayKey, usageDelta);
102
106
  }
@@ -0,0 +1,244 @@
1
+ const AUTO = 'auto';
2
+
3
+ /**
4
+ * Overview ingest animation control panel.
5
+ *
6
+ * This is the single source of truth for:
7
+ * - Overview client-side presentation timing
8
+ * - Overview ECharts timing defaults
9
+ * - Overview server-side tail pacing
10
+ *
11
+ * `speed` scales the main timings:
12
+ * - `1` = baseline
13
+ * - `2` = 2x faster
14
+ * - `0.5` = 2x slower
15
+ *
16
+ * Per-chart overrides use:
17
+ * - `'auto'` to inherit the scaled main value
18
+ * - a number to force an explicit value in ms
19
+ */
20
+ export const OVERVIEW_INGEST_ANIMATION = {
21
+ speed: 1,
22
+ main: {
23
+ presentationDurationMs: 190,
24
+ chartAppearDurationMs: 0,
25
+ chartUpdateDurationMs: 200,
26
+ easing: 'linear',
27
+ easingUpdate: 'linear',
28
+ },
29
+ tail: {
30
+ // Master switch for the end-of-ingest slowdown behavior.
31
+ enabled: true,
32
+ // Progress threshold where the tail mode starts, expressed from 0..1.
33
+ startPercent: 0.85,
34
+ // Fixed presentation duration in ms during the tail; use AUTO to derive it from main.durationScale.
35
+ durationMs: AUTO,
36
+ // Multiplier applied to the normal presentation duration when durationMs is AUTO.
37
+ durationScale: 100,
38
+ // Shared easing applied by the Overview presentation animator during the tail.
39
+ easing: 'cubicOut',
40
+ // Backend Overview live-update cadence during the tail, in patches per second.
41
+ overviewHz: 5,
42
+ },
43
+ daily: {
44
+ chartAppearDurationMs: AUTO,
45
+ chartUpdateDurationMs: 30,
46
+ easing: AUTO,
47
+ easingUpdate: AUTO,
48
+ },
49
+ bars: {
50
+ chartAppearDurationMs: AUTO,
51
+ chartUpdateDurationMs: 20,
52
+ easing: AUTO,
53
+ easingUpdate: AUTO,
54
+ },
55
+ donuts: {
56
+ chartAppearDurationMs: AUTO,
57
+ chartUpdateDurationMs: AUTO,
58
+ easing: AUTO,
59
+ easingUpdate: 'linear',
60
+ seriesAnimation: false,
61
+ },
62
+ heatmap: {
63
+ /** Duration (ms) of the pop animation when a cell gets new data during ingest */
64
+ popDurationMs: 380,
65
+ /** Don't trigger pop when ingest progress is above this (avoids burst at settle) */
66
+ settleThreshold: 0.998,
67
+ /** Min relative intensity rise (0–1) to trigger pop – e.g. 0.08 = cell turning white */
68
+ intensityRiseThreshold: 0.08,
69
+ /** Relative intensity (0–1) that counts as "white" – crossing this triggers pop */
70
+ whiteThreshold: 0.88,
71
+ /** Cell is "new winner" if val >= maxVal * this when max increased */
72
+ nearMaxRatio: 0.985,
73
+ },
74
+ videoExport: {
75
+ width: 1080,
76
+ height: 864,
77
+ fps: 60,
78
+ introDurationMs: 900,
79
+ replayDurationMs: 8000,
80
+ tailDurationMs: 5000,
81
+ tailReplayFraction: 0.72,
82
+ captureFormat: 'png',
83
+ jpegQuality: 92,
84
+ crf: 10,
85
+ encoderPreset: 'veryfast',
86
+ },
87
+ };
88
+
89
+ /** Shared ECharts animation defaults for non-Overview charts */
90
+ export const ECHARTS_ANIMATION = {
91
+ animationDuration: 750,
92
+ animationDurationUpdate: 220,
93
+ animationEasing: 'cubicOut',
94
+ animationEasingUpdate: 'cubicOut',
95
+ };
96
+
97
+ /** Label animation - fade in after chart finishes */
98
+ export const ECHARTS_LABEL_ANIMATION = {
99
+ show: true,
100
+ animationDuration: 250,
101
+ animationDurationUpdate: 220,
102
+ animationDelay: 250,
103
+ animationDelayUpdate: 180,
104
+ animationEasing: 'cubicOut',
105
+ animationEasingUpdate: 'cubicOut',
106
+ };
107
+
108
+ /** Detail donut charts (Repos/Models/Daily) */
109
+ export const ECHARTS_DONUT_ANIMATION = {
110
+ ...ECHARTS_ANIMATION,
111
+ animationDurationUpdate: 200,
112
+ animationDelayUpdate: 0,
113
+ animationEasingUpdate: 'cubicOut',
114
+ };
115
+
116
+ /** Detail bar charts (Repos/Models/Daily) */
117
+ export const ECHARTS_DETAIL_BAR_ANIMATION = {
118
+ animationDuration: 700,
119
+ animationDurationUpdate: 220,
120
+ animationDelay: 250,
121
+ animationDelayUpdate: 140,
122
+ animationEasing: 'cubicOut',
123
+ animationEasingUpdate: 'cubicOut',
124
+ };
125
+
126
+ /** Detail bar labels */
127
+ export const ECHARTS_DETAIL_BAR_LABEL_ANIMATION = {
128
+ ...ECHARTS_LABEL_ANIMATION,
129
+ animationDelay: 950,
130
+ animationDelayUpdate: 360,
131
+ };
132
+
133
+ function safeSpeed(value) {
134
+ return Number.isFinite(value) && value > 0 ? value : 1;
135
+ }
136
+
137
+ function scaledMainDuration(ms) {
138
+ return Math.max(0, Math.round(ms / safeSpeed(OVERVIEW_INGEST_ANIMATION.speed)));
139
+ }
140
+
141
+ function resolveOverviewValue(overrideValue, mainValue) {
142
+ return overrideValue === AUTO ? scaledMainDuration(mainValue) : overrideValue;
143
+ }
144
+
145
+ function resolveOverviewString(overrideValue, mainValue) {
146
+ return overrideValue === AUTO ? mainValue : overrideValue;
147
+ }
148
+
149
+ function buildOverviewChartAnimation(overrides) {
150
+ return {
151
+ animation: true,
152
+ animationDuration: resolveOverviewValue(
153
+ overrides.chartAppearDurationMs,
154
+ OVERVIEW_INGEST_ANIMATION.main.chartAppearDurationMs
155
+ ),
156
+ animationDurationUpdate: resolveOverviewValue(
157
+ overrides.chartUpdateDurationMs,
158
+ OVERVIEW_INGEST_ANIMATION.main.chartUpdateDurationMs
159
+ ),
160
+ animationEasing: resolveOverviewString(
161
+ overrides.easing,
162
+ OVERVIEW_INGEST_ANIMATION.main.easing
163
+ ),
164
+ animationEasingUpdate: resolveOverviewString(
165
+ overrides.easingUpdate,
166
+ OVERVIEW_INGEST_ANIMATION.main.easingUpdate
167
+ ),
168
+ };
169
+ }
170
+
171
+ export const OVERVIEW_PRESENTATION_DURATION_MS = scaledMainDuration(
172
+ OVERVIEW_INGEST_ANIMATION.main.presentationDurationMs
173
+ );
174
+
175
+ export function resolveOverviewPresentationDuration(progress = 0, isIngestActive = false) {
176
+ const base = OVERVIEW_PRESENTATION_DURATION_MS;
177
+ const tail = OVERVIEW_INGEST_ANIMATION.tail;
178
+
179
+ if (!isIngestActive || !tail?.enabled) return base;
180
+
181
+ const start = Math.min(Math.max(tail.startPercent ?? 0.9, 0), 0.999);
182
+ const clampedProgress = Math.min(Math.max(progress || 0, 0), 1);
183
+ if (clampedProgress <= start) return base;
184
+
185
+ const normalized = (clampedProgress - start) / Math.max(1 - start, 0.001);
186
+ const durationTarget = tail.durationMs === AUTO
187
+ ? Math.round(base * Math.max(tail.durationScale || 1, 1))
188
+ : tail.durationMs;
189
+
190
+ return Math.round(base + (durationTarget - base) * cubicOut(normalized));
191
+ }
192
+
193
+ export function resolveOverviewPresentationEasing(progress = 0, isIngestActive = false) {
194
+ const tail = OVERVIEW_INGEST_ANIMATION.tail;
195
+ if (!isIngestActive || !tail?.enabled) return 'linear';
196
+
197
+ const start = Math.min(Math.max(tail.startPercent ?? 0.9, 0), 0.999);
198
+ const clampedProgress = Math.min(Math.max(progress || 0, 0), 1);
199
+ if (clampedProgress <= start) return 'linear';
200
+
201
+ return tail.easing || 'cubicOut';
202
+ }
203
+
204
+ export function isOverviewTailActive(progress = 0, isIngestActive = false) {
205
+ const tail = OVERVIEW_INGEST_ANIMATION.tail;
206
+ if (!isIngestActive || !tail?.enabled) return false;
207
+
208
+ const start = Math.min(Math.max(tail.startPercent ?? 0.9, 0), 0.999);
209
+ const clampedProgress = Math.min(Math.max(progress || 0, 0), 1);
210
+ return clampedProgress > start;
211
+ }
212
+
213
+ /** Overview page - resolved main animation config */
214
+ export const ECHARTS_OVERVIEW_ANIMATION = buildOverviewChartAnimation({
215
+ chartAppearDurationMs: AUTO,
216
+ chartUpdateDurationMs: AUTO,
217
+ easing: AUTO,
218
+ easingUpdate: AUTO,
219
+ });
220
+
221
+ /** Overview DailySpark - compact stacked bar */
222
+ export const ECHARTS_OVERVIEW_DAILY = buildOverviewChartAnimation(
223
+ OVERVIEW_INGEST_ANIMATION.daily
224
+ );
225
+
226
+ /** Overview Top Repos - horizontal bar chart */
227
+ export const ECHARTS_OVERVIEW_BARS = buildOverviewChartAnimation(
228
+ OVERVIEW_INGEST_ANIMATION.bars
229
+ );
230
+
231
+ /** Overview Work Type & Models - donut charts */
232
+ export const ECHARTS_OVERVIEW_DONUTS = buildOverviewChartAnimation(
233
+ OVERVIEW_INGEST_ANIMATION.donuts
234
+ );
235
+
236
+ export const ECHARTS_OVERVIEW_DONUT_SERIES_ANIMATION =
237
+ OVERVIEW_INGEST_ANIMATION.donuts.seriesAnimation;
238
+
239
+ export { AUTO as OVERVIEW_ANIMATION_AUTO };
240
+
241
+ function cubicOut(t) {
242
+ const x = Math.min(Math.max(t, 0), 1);
243
+ return 1 - Math.pow(1 - x, 3);
244
+ }
@@ -1 +0,0 @@
1
- :root{--bg-base: #06080f;--bg-surface: #0d1117;--bg-card: #161b22;--bg-card-hover: #1c2333;--bg-elevated: #21262d;--border: #30363d;--border-accent: rgba(99, 102, 241, .25);--text-primary: #e6edf3;--text-secondary: #8b949e;--text-muted: #484f58;--accent: #6366f1;--accent-dim: rgba(99, 102, 241, .12);--green: #3fb950;--yellow: #d29922;--red: #f85149;--orange: #f97316;--cyan: #39d2f5;--pink: #f472b6;--font-sans: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;--font-mono: "JetBrains Mono", "Fira Code", monospace;--radius: 8px;--radius-lg: 12px}*{box-sizing:border-box;margin:0;padding:0}body{font-family:var(--font-sans);background:var(--bg-base);color:var(--text-primary);line-height:1.6;-webkit-font-smoothing:antialiased}.app{min-height:100vh;display:flex;flex-direction:column}.app-content{opacity:0;display:flex;flex-direction:column;min-height:100vh}.app-content-revealed{opacity:1;animation:contentReveal .5s ease forwards}@keyframes contentReveal{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.navbar{background:var(--bg-surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}.navbar-inner{display:flex;align-items:center;gap:1.5rem;padding:0 2rem;height:48px;max-width:1400px;margin:0 auto;width:100%;box-sizing:border-box}.navbar-brand{font-weight:700;font-size:.95rem;letter-spacing:-.03em;background:linear-gradient(135deg,#818cf8,#6366f1);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.navbar-tabs{display:flex;gap:2px}.navbar-tab{padding:.35rem .85rem;border-radius:6px;cursor:pointer;font-size:.8rem;font-weight:500;color:var(--text-secondary);background:none;border:none;transition:all .15s}.navbar-tab:hover{color:var(--text-primary);background:var(--bg-card)}.navbar-tab.active{color:#fff;background:var(--accent)}.navbar-meta{margin-left:auto;display:flex;align-items:center;gap:.75rem;flex:1;min-width:0}.navbar-ingest-wrap{display:flex;align-items:center;gap:.75rem;flex:1;min-width:0;transition:opacity .3s ease}.navbar-ingest-wrap.navbar-ingest-fade-out{opacity:0;pointer-events:none}.navbar-date-wrap{display:flex;align-items:center;gap:.75rem;margin-left:auto;flex-shrink:0;transition:opacity .3s ease}.navbar-date-wrap-dimmed{opacity:.5;pointer-events:none}.navbar-date{display:inline-block;width:22ch;min-width:22ch;font-variant-numeric:tabular-nums;text-align:right;overflow:hidden;text-overflow:ellipsis}.navbar-progress-wrap{flex:1;height:4px;background:var(--bg-elevated);border-radius:2px;overflow:hidden}.navbar-progress-bar{height:100%;background:linear-gradient(90deg,var(--accent),#818cf8);border-radius:2px;transition:width .4s ease}.navbar-status{font-size:.7rem;color:var(--text-muted);font-family:var(--font-mono)}.main-content{flex:1;padding:1.5rem 2rem 3rem;max-width:1400px;margin:0 auto;width:100%;min-width:0;container-type:inline-size;container-name:main}.app-footer{padding:.75rem 2rem 1rem;font-size:.7rem;color:var(--text-muted);text-align:center;max-width:1400px;margin:auto auto 0;width:100%;box-sizing:border-box}.section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem}.section-title{font-size:1rem;font-weight:600;letter-spacing:-.01em}.stat-row{display:flex;flex-wrap:wrap;gap:1rem;align-items:stretch;margin-bottom:1.5rem}.stat-cards{display:grid;grid-template-columns:repeat(4,minmax(160px,1fr));gap:.75rem;flex:0 0 auto;min-width:0}@media(max-width:920px){.stat-cards{flex:1 1 auto;grid-template-columns:repeat(2,minmax(160px,1fr))}}@media(max-width:500px){.stat-cards{grid-template-columns:1fr}}.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:1rem 1.1rem;transition:border-color .2s}.stat-card:hover{border-color:var(--border-accent)}.stat-label{font-size:.65rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);margin-bottom:.35rem}.stat-value{font-size:1.6rem;font-weight:700;font-family:var(--font-mono);letter-spacing:-.03em;line-height:1.1}.stat-sub{font-size:.72rem;color:var(--text-muted);margin-top:.2rem}.stat-per-day{font-size:.72rem;color:var(--text-muted);margin-top:.25rem;font-family:var(--font-mono)}.stat-per-day .stat-per-day-value{font-weight:600;color:var(--text-secondary)}.range-toggle{display:inline-flex;background:var(--bg-card);border:1px solid var(--border);border-radius:6px;overflow:hidden}.range-btn{padding:.3rem .7rem;font-size:.72rem;font-weight:600;color:var(--text-muted);background:none;border:none;cursor:pointer;transition:all .15s}.range-btn:hover{color:var(--text-secondary)}.range-btn.active{color:#fff;background:var(--accent)}.heatmap-wrap{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.25rem;margin-bottom:1.5rem;overflow-x:auto}.heatmap-grid{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,1fr);gap:3px}.heatmap-cell{width:13px;height:13px;border-radius:2px;background:var(--bg-elevated);opacity:.4;transform:scale(.82);transition:background .18s ease,opacity .22s ease,transform .22s ease}.heatmap-cell.active{opacity:1;transform:scale(1)}.heatmap-cell:hover{outline:1px solid var(--text-muted)}.heatmap-labels{display:flex;justify-content:space-between;margin-top:.5rem;font-size:.65rem;color:var(--text-muted)}.chart-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.25rem;margin-bottom:1.5rem;position:relative}.chart-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem}.chart-title{font-size:.85rem;font-weight:600}.chart-badge{font-size:.62rem;color:var(--yellow);background:#d299221a;padding:.15rem .5rem;border-radius:4px}.export-btn{position:absolute;top:.75rem;right:.75rem;padding:.25rem .5rem;border-radius:4px;border:1px solid var(--border);background:var(--bg-surface);color:var(--text-muted);font-size:.65rem;cursor:pointer;opacity:0;transition:all .15s;z-index:2}.chart-card:hover .export-btn{opacity:.7}.export-btn:hover{opacity:1!important;color:var(--text-primary);border-color:var(--accent)}.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:1.25rem;margin-bottom:1.5rem;min-width:0}.grid-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1.25rem;margin-bottom:1.5rem;min-width:0}.grid-3>.chart-card{min-width:0;overflow:hidden}@media(max-width:900px){.grid-2,.grid-3{grid-template-columns:1fr}}.btn-group{display:flex;gap:2px}.btn{padding:.3rem .65rem;border-radius:5px;border:1px solid var(--border);background:var(--bg-surface);color:var(--text-muted);font-size:.72rem;font-weight:500;cursor:pointer;transition:all .12s}.btn:hover{color:var(--text-secondary);border-color:var(--text-muted)}.btn.active{color:#fff;background:var(--accent);border-color:var(--accent)}.icon-btn{width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text-muted);font-size:.9rem;line-height:1;cursor:pointer;transition:color .12s,border-color .12s,background .12s,transform .12s}.icon-btn:hover:not(:disabled){color:var(--text-primary);border-color:var(--accent);background:var(--bg-elevated);transform:rotate(20deg)}.icon-btn:disabled{opacity:.55;cursor:wait}.overview-daily-spark{flex:1 1 140px;min-width:140px;min-height:0;position:relative;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:visible}.overview-daily-spark-title{position:absolute;top:1rem;left:1.1rem;font-size:.65rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);z-index:1;pointer-events:none}.overview-daily-spark-chart{position:absolute;top:0;right:0;bottom:0;left:0;overflow:hidden}.overview-daily-spark-empty{display:flex;align-items:center;justify-content:center}.overview-daily-spark-empty-text{font-size:.7rem;color:var(--text-muted)}@media(max-width:920px){.overview-daily-spark{flex:1 1 100%;min-width:100%;min-height:140px}}.table-wrap{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden}.model-detail-wrap{display:flex;flex-direction:column;gap:.85rem}.model-detail-note{color:var(--text-muted);font-size:.74rem;line-height:1.4}.model-detail-footer{display:flex;justify-content:flex-start;margin-top:.25rem}.model-detail-toggle{display:inline-flex;align-items:center;gap:.35rem;width:fit-content;padding:.35rem .6rem;font-size:.72rem;font-weight:500;color:var(--text-muted);background:#ffffff0a;border:1px solid var(--border);border-radius:6px;cursor:pointer;transition:color .2s,background .2s,border-color .2s}.model-detail-toggle:hover{color:var(--text-secondary);background:#ffffff0f;border-color:#6366f14d}.model-detail-toggle-text{font-family:var(--font-sans)}.model-detail-toggle-arrow{font-size:.6rem;opacity:.8;transition:transform .25s ease}.model-detail-toggle-arrow.expanded{transform:rotate(180deg)}.model-detail-charts{display:flex;gap:.25rem;flex-wrap:wrap;align-items:flex-start;width:100%;overflow:visible;position:relative;z-index:10}.model-detail-donut{flex:0 0 200px;width:200px;min-width:200px;height:200px;overflow:visible;z-index:10;display:flex;flex-direction:column}.model-detail-donut .chart-title{flex-shrink:0}.model-detail-donut .echarts-for-react{flex:1;min-height:0}.model-detail-wrap .model-detail-donut{flex:0 0 340px;width:340px;min-width:340px;height:220px}.model-detail-donut .echarts-for-react,.model-detail-donut .echarts-for-react>div,.model-detail-bar .echarts-for-react,.model-detail-bar .echarts-for-react>div{overflow:visible!important}.model-detail-bar{flex:1 1 300px;min-width:300px;height:200px;overflow:visible;display:flex;flex-direction:column}.model-detail-bar .chart-title{flex-shrink:0}.model-detail-bar .echarts-for-react{flex:1;min-height:0}.model-detail-summary-wrap{max-height:0;overflow:hidden;transition:max-height .2s ease-out}.model-detail-summary-wrap.expanded{max-height:400px}.model-detail-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.6rem;padding-top:.5rem}.model-detail-summary-card{background:#ffffff05;border:1px solid var(--border);border-radius:10px;padding:.65rem .75rem}.model-detail-summary-head{display:flex;align-items:center;gap:.45rem;margin-bottom:.45rem;font-size:.76rem;font-weight:600;color:var(--text-primary);text-transform:capitalize;min-width:0}.model-detail-summary-head .model-detail-summary-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.model-detail-swatch{width:9px;height:9px;border-radius:999px;flex:0 0 auto}.model-detail-summary-line{font-size:.72rem;color:var(--text-muted);line-height:1.45}.detail-summary-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-bottom:1rem}.detail-summary-stat{background:#ffffff08;border:1px solid var(--border);border-radius:10px;padding:.8rem .9rem}.detail-summary-label{font-size:.68rem;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:.25rem}.detail-summary-value{font-size:1.1rem;font-family:var(--font-mono);color:var(--text-primary)}.table-search{padding:.75rem 1rem;border-bottom:1px solid var(--border)}.table-search input{width:100%;padding:.5rem .85rem;border-radius:6px;border:1px solid var(--border);background:var(--bg-surface);color:var(--text-primary);font-size:.82rem;font-family:var(--font-sans);outline:none}.table-search input:focus{border-color:var(--accent)}.table-search input::placeholder{color:var(--text-muted)}table{width:100%;border-collapse:collapse;font-size:.78rem}th{text-align:left;padding:.55rem .85rem;font-weight:600;color:var(--text-muted);border-bottom:1px solid var(--border);font-size:.68rem;text-transform:uppercase;letter-spacing:.05em;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}td{padding:.5rem .85rem;border-bottom:1px solid rgba(48,54,61,.5);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}tr:hover td{color:var(--text-primary);background:#6366f108}.tag{display:inline-block;padding:.1rem .4rem;border-radius:4px;font-size:.66rem;font-family:var(--font-mono);font-weight:500}.tag-review{background:#06b6d41f;color:var(--cyan)}.tag-exploration{background:#f973161f;color:var(--orange)}.tag-planning{background:#eab3081f;color:var(--yellow)}.tag-memory{background:#ec48991f;color:var(--pink)}.tag-generic{background:#64748b1f;color:var(--text-secondary)}.loading-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#06080feb;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:999;transition:opacity .6s ease}.loading-overlay.fading{opacity:0;pointer-events:none}.loading-logo{font-size:1.5rem;font-weight:700;background:linear-gradient(135deg,#818cf8,#6366f1);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:1.5rem}.loading-bar-wrap{width:260px;height:4px;background:var(--bg-elevated);border-radius:2px;overflow:hidden;margin-bottom:1rem}.loading-bar{height:100%;background:linear-gradient(90deg,var(--accent),#818cf8);border-radius:2px;transition:width .4s ease}.loading-phase{font-size:.82rem;font-weight:500;color:var(--text-secondary);margin-bottom:.2rem}.loading-detail{font-family:var(--font-mono);font-size:.72rem;color:var(--text-muted)}.loading-footer{position:absolute;bottom:1.5rem;font-size:.7rem;color:var(--text-muted)}.loading-heart{color:#f43f5e}.coverage-subtle{display:flex;align-items:center;gap:1.5rem;font-size:.68rem;color:var(--text-muted);padding:.5rem 0;margin-bottom:1rem}.coverage-item{display:flex;align-items:center;gap:.35rem;font-variant-numeric:tabular-nums}.coverage-nums{display:inline-block;min-width:9ch;text-align:right}.coverage-nums-single{min-width:4ch}.coverage-dot{width:6px;height:6px;border-radius:50%}html{scrollbar-width:none}::-webkit-scrollbar{display:none;width:0;height:0}@keyframes fadeUp{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.animate-in{animation:fadeUp .35s ease forwards}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.incomplete-badge{font-size:.65rem;color:var(--yellow);background:#d299221a;padding:.12rem .45rem;border-radius:3px;animation:pulse 2s infinite}.ingesting-badge .ingesting-pct{display:inline-block;min-width:4ch;text-align:right;font-variant-numeric:tabular-nums}.echarts-tooltip,[class*=ec-component-tooltip]{max-width:90vw!important;max-height:80vh!important;overflow:auto!important}