@boshu2/vibe-check 1.6.2 → 1.7.0
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/.agents/bundles/automatic-learning-cadence-plan-2025-12-02.md +1297 -0
- package/.agents/bundles/automatic-learning-cadence-research-2025-12-02.md +481 -0
- package/.agents/bundles/dashboard-data-quality-plan.md +458 -0
- package/.agents/bundles/rating-scoring-alignment-plan.md +427 -0
- package/.agents/bundles/rpi-session-capture-plan-2025-12-02.md +693 -0
- package/.agents/bundles/rpi-session-capture-research-2025-12-02.md +433 -0
- package/.agents/bundles/session-integration-plan-2025-12-02.md +144 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +74 -2
- package/Makefile +173 -0
- package/README.md +33 -0
- package/claude-progress.json +34 -5
- package/claude-progress.txt +66 -0
- package/dashboard/app.js +699 -66
- package/dashboard/chart.min.js +20 -0
- package/dashboard/dashboard-data.js +764 -0
- package/dashboard/dashboard-data.json +182 -71
- package/dashboard/index.html +139 -14
- package/dashboard/styles.css +579 -4
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +38 -2
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/dashboard.js +4 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +3 -3
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/learn.d.ts +3 -0
- package/dist/commands/learn.d.ts.map +1 -0
- package/dist/commands/learn.js +161 -0
- package/dist/commands/learn.js.map +1 -0
- package/dist/commands/lesson.d.ts +8 -0
- package/dist/commands/lesson.d.ts.map +1 -0
- package/dist/commands/lesson.js +206 -0
- package/dist/commands/lesson.js.map +1 -0
- package/dist/commands/profile.d.ts.map +1 -1
- package/dist/commands/profile.js +3 -202
- package/dist/commands/profile.js.map +1 -1
- package/dist/commands/session.d.ts +51 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +561 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/gamification/index.d.ts +1 -3
- package/dist/gamification/index.d.ts.map +1 -1
- package/dist/gamification/index.js +2 -5
- package/dist/gamification/index.js.map +1 -1
- package/dist/gamification/pattern-memory.d.ts +1 -1
- package/dist/gamification/pattern-memory.d.ts.map +1 -1
- package/dist/gamification/pattern-memory.js.map +1 -1
- package/dist/gamification/profile.d.ts +2 -2
- package/dist/gamification/profile.d.ts.map +1 -1
- package/dist/gamification/profile.js +2 -15
- package/dist/gamification/profile.js.map +1 -1
- package/dist/gamification/types.d.ts +8 -2
- package/dist/gamification/types.d.ts.map +1 -1
- package/dist/gamification/types.js.map +1 -1
- package/dist/insights/index.d.ts.map +1 -1
- package/dist/insights/index.js +16 -4
- package/dist/insights/index.js.map +1 -1
- package/dist/insights/types.d.ts +14 -0
- package/dist/insights/types.d.ts.map +1 -1
- package/dist/learning/cadence.d.ts +15 -0
- package/dist/learning/cadence.d.ts.map +1 -0
- package/dist/learning/cadence.js +130 -0
- package/dist/learning/cadence.js.map +1 -0
- package/dist/learning/index.d.ts +19 -0
- package/dist/learning/index.d.ts.map +1 -0
- package/dist/learning/index.js +35 -0
- package/dist/learning/index.js.map +1 -0
- package/dist/learning/lessons-storage.d.ts +48 -0
- package/dist/learning/lessons-storage.d.ts.map +1 -0
- package/dist/learning/lessons-storage.js +266 -0
- package/dist/learning/lessons-storage.js.map +1 -0
- package/dist/learning/lessons-types.d.ts +83 -0
- package/dist/learning/lessons-types.d.ts.map +1 -0
- package/dist/learning/lessons-types.js +15 -0
- package/dist/learning/lessons-types.js.map +1 -0
- package/dist/learning/nudges.d.ts +20 -0
- package/dist/learning/nudges.d.ts.map +1 -0
- package/dist/learning/nudges.js +68 -0
- package/dist/learning/nudges.js.map +1 -0
- package/dist/learning/retrospective.d.ts +27 -0
- package/dist/learning/retrospective.d.ts.map +1 -0
- package/dist/learning/retrospective.js +184 -0
- package/dist/learning/retrospective.js.map +1 -0
- package/dist/learning/storage.d.ts +44 -0
- package/dist/learning/storage.d.ts.map +1 -0
- package/dist/learning/storage.js +194 -0
- package/dist/learning/storage.js.map +1 -0
- package/dist/learning/surfacing.d.ts +36 -0
- package/dist/learning/surfacing.d.ts.map +1 -0
- package/dist/learning/surfacing.js +255 -0
- package/dist/learning/surfacing.js.map +1 -0
- package/dist/learning/synthesis.d.ts +17 -0
- package/dist/learning/synthesis.d.ts.map +1 -0
- package/dist/learning/synthesis.js +293 -0
- package/dist/learning/synthesis.js.map +1 -0
- package/dist/learning/types.d.ts +60 -0
- package/dist/learning/types.d.ts.map +1 -0
- package/dist/learning/types.js +17 -0
- package/dist/learning/types.js.map +1 -0
- package/docs/METRICS.md +528 -0
- package/feature-list.json +21 -0
- package/package.json +1 -1
package/dashboard/app.js
CHANGED
|
@@ -4,6 +4,11 @@ class VibeDashboard {
|
|
|
4
4
|
this.profile = null;
|
|
5
5
|
this.charts = {};
|
|
6
6
|
this.currentPage = 'dashboard';
|
|
7
|
+
this.historyFilters = {
|
|
8
|
+
rating: 'all',
|
|
9
|
+
sortBy: 'date-desc',
|
|
10
|
+
range: 'all'
|
|
11
|
+
};
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
async init() {
|
|
@@ -50,14 +55,77 @@ class VibeDashboard {
|
|
|
50
55
|
closeModal.addEventListener('click', () => this.closeModal());
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
// Session modal close
|
|
59
|
+
const closeSessionModal = document.getElementById('closeSessionModal');
|
|
60
|
+
if (closeSessionModal) {
|
|
61
|
+
closeSessionModal.addEventListener('click', () => this.closeSessionModal());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// History filters
|
|
65
|
+
['filterRating', 'sortBy', 'filterRange'].forEach(id => {
|
|
66
|
+
const el = document.getElementById(id);
|
|
67
|
+
if (el) {
|
|
68
|
+
el.addEventListener('change', (e) => {
|
|
69
|
+
const key = id === 'filterRating' ? 'rating' :
|
|
70
|
+
id === 'sortBy' ? 'sortBy' : 'range';
|
|
71
|
+
this.historyFilters[key] = e.target.value;
|
|
72
|
+
this.renderHistory();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// View All link
|
|
78
|
+
const viewAllLink = document.querySelector('.view-all');
|
|
79
|
+
if (viewAllLink) {
|
|
80
|
+
viewAllLink.addEventListener('click', (e) => {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
this.navigateTo('history');
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
53
86
|
// Keyboard shortcuts
|
|
54
87
|
document.addEventListener('keydown', (e) => {
|
|
55
|
-
if (e.key === 'Escape')
|
|
88
|
+
if (e.key === 'Escape') {
|
|
89
|
+
this.closeModal();
|
|
90
|
+
this.closeSessionModal();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Click outside modal to close
|
|
95
|
+
document.getElementById('sessionModal')?.addEventListener('click', (e) => {
|
|
96
|
+
if (e.target.classList.contains('modal-overlay')) {
|
|
97
|
+
this.closeSessionModal();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Metrics help buttons
|
|
102
|
+
document.getElementById('codeHealthHelp')?.addEventListener('click', () => {
|
|
103
|
+
this.showMetricsHelp('codeHealth');
|
|
104
|
+
});
|
|
105
|
+
document.getElementById('patternScoreHelp')?.addEventListener('click', () => {
|
|
106
|
+
this.showMetricsHelp('patternScore');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Metrics modal close
|
|
110
|
+
document.getElementById('closeMetricsModal')?.addEventListener('click', () => {
|
|
111
|
+
this.closeMetricsModal();
|
|
112
|
+
});
|
|
113
|
+
document.getElementById('metricsModal')?.addEventListener('click', (e) => {
|
|
114
|
+
if (e.target.classList.contains('modal-overlay')) {
|
|
115
|
+
this.closeMetricsModal();
|
|
116
|
+
}
|
|
56
117
|
});
|
|
57
118
|
}
|
|
58
119
|
|
|
59
120
|
async loadProfile() {
|
|
60
|
-
//
|
|
121
|
+
// First check for global variable (works with file:// URLs)
|
|
122
|
+
if (window.VIBE_CHECK_DATA) {
|
|
123
|
+
this.dashboardData = window.VIBE_CHECK_DATA;
|
|
124
|
+
this.profile = this.transformToProfile(window.VIBE_CHECK_DATA);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Try to fetch dashboard-data.json (works with http:// URLs)
|
|
61
129
|
try {
|
|
62
130
|
const response = await fetch('dashboard-data.json');
|
|
63
131
|
if (response.ok) {
|
|
@@ -75,7 +143,7 @@ class VibeDashboard {
|
|
|
75
143
|
if (stored) {
|
|
76
144
|
this.profile = JSON.parse(stored);
|
|
77
145
|
} else {
|
|
78
|
-
this.profile = this.
|
|
146
|
+
this.profile = this.getEmptyProfile();
|
|
79
147
|
}
|
|
80
148
|
}
|
|
81
149
|
|
|
@@ -92,7 +160,11 @@ class VibeDashboard {
|
|
|
92
160
|
},
|
|
93
161
|
streak: data.profile.streak,
|
|
94
162
|
achievements: data.achievements.filter(a => a.unlockedAt),
|
|
95
|
-
|
|
163
|
+
// Map 'rating' to 'overall' for UI compatibility
|
|
164
|
+
sessions: data.sessions.map(s => ({
|
|
165
|
+
...s,
|
|
166
|
+
overall: s.rating || s.overall,
|
|
167
|
+
})),
|
|
96
168
|
stats: {
|
|
97
169
|
totalSessions: data.stats.totals.sessions,
|
|
98
170
|
totalCommitsAnalyzed: data.stats.totals.commits,
|
|
@@ -103,43 +175,31 @@ class VibeDashboard {
|
|
|
103
175
|
};
|
|
104
176
|
}
|
|
105
177
|
|
|
106
|
-
|
|
107
|
-
//
|
|
178
|
+
getEmptyProfile() {
|
|
179
|
+
// Empty profile for new users - no fake data
|
|
108
180
|
return {
|
|
109
181
|
version: '1.0.0',
|
|
110
182
|
xp: {
|
|
111
|
-
total:
|
|
112
|
-
level:
|
|
113
|
-
levelName: '
|
|
114
|
-
currentLevelXP:
|
|
115
|
-
nextLevelXP:
|
|
183
|
+
total: 0,
|
|
184
|
+
level: 1,
|
|
185
|
+
levelName: 'Novice',
|
|
186
|
+
currentLevelXP: 0,
|
|
187
|
+
nextLevelXP: 100,
|
|
116
188
|
},
|
|
117
189
|
streak: {
|
|
118
|
-
current:
|
|
119
|
-
longest:
|
|
120
|
-
weeklyProgress:
|
|
190
|
+
current: 0,
|
|
191
|
+
longest: 0,
|
|
192
|
+
weeklyProgress: 0,
|
|
121
193
|
weeklyGoal: 5,
|
|
122
194
|
},
|
|
123
|
-
achievements: [
|
|
124
|
-
|
|
125
|
-
{ id: 'elite_vibes', name: 'Elite Vibes', icon: '✨', unlockedAt: '2025-11-25' },
|
|
126
|
-
{ id: 'zen_master', name: 'Zen Master', icon: '🧘', unlockedAt: '2025-11-28' },
|
|
127
|
-
],
|
|
128
|
-
sessions: [
|
|
129
|
-
{ date: '2025-11-29', vibeScore: 89, overall: 'ELITE', commits: 14, spirals: 0 },
|
|
130
|
-
{ date: '2025-11-28', vibeScore: 85, overall: 'HIGH', commits: 23, spirals: 1 },
|
|
131
|
-
{ date: '2025-11-27', vibeScore: 91, overall: 'ELITE', commits: 31, spirals: 0 },
|
|
132
|
-
{ date: '2025-11-26', vibeScore: 78, overall: 'HIGH', commits: 18, spirals: 2 },
|
|
133
|
-
{ date: '2025-11-25', vibeScore: 82, overall: 'HIGH', commits: 27, spirals: 1 },
|
|
134
|
-
{ date: '2025-11-24', vibeScore: 70, overall: 'MEDIUM', commits: 12, spirals: 3 },
|
|
135
|
-
{ date: '2025-11-23', vibeScore: 88, overall: 'ELITE', commits: 35, spirals: 0 },
|
|
136
|
-
],
|
|
195
|
+
achievements: [],
|
|
196
|
+
sessions: [],
|
|
137
197
|
stats: {
|
|
138
|
-
totalSessions:
|
|
139
|
-
totalCommitsAnalyzed:
|
|
140
|
-
avgVibeScore:
|
|
141
|
-
bestVibeScore:
|
|
142
|
-
spiralsAvoided:
|
|
198
|
+
totalSessions: 0,
|
|
199
|
+
totalCommitsAnalyzed: 0,
|
|
200
|
+
avgVibeScore: 0,
|
|
201
|
+
bestVibeScore: 0,
|
|
202
|
+
spiralsAvoided: 0,
|
|
143
203
|
},
|
|
144
204
|
};
|
|
145
205
|
}
|
|
@@ -172,12 +232,28 @@ class VibeDashboard {
|
|
|
172
232
|
}
|
|
173
233
|
|
|
174
234
|
renderDashboard() {
|
|
235
|
+
this.updateRepoName();
|
|
175
236
|
this.updateProfileSummary();
|
|
176
237
|
this.updateStats();
|
|
177
238
|
this.renderRecentSessions();
|
|
178
239
|
this.renderInsights();
|
|
179
240
|
}
|
|
180
241
|
|
|
242
|
+
updateRepoName() {
|
|
243
|
+
const repoEl = document.querySelector('#repoName .repo-path');
|
|
244
|
+
if (!repoEl) return;
|
|
245
|
+
|
|
246
|
+
if (this.dashboardData?.repo) {
|
|
247
|
+
// Show just the repo name, not full path
|
|
248
|
+
const parts = this.dashboardData.repo.split('/');
|
|
249
|
+
const repoName = parts[parts.length - 1] || this.dashboardData.repo;
|
|
250
|
+
repoEl.textContent = repoName;
|
|
251
|
+
repoEl.title = this.dashboardData.repo; // Full path on hover
|
|
252
|
+
} else {
|
|
253
|
+
repoEl.textContent = 'No repo data';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
181
257
|
updateProfileSummary() {
|
|
182
258
|
const { xp, streak } = this.profile;
|
|
183
259
|
|
|
@@ -198,10 +274,17 @@ class VibeDashboard {
|
|
|
198
274
|
const { sessions, stats } = this.profile;
|
|
199
275
|
const latest = sessions[0];
|
|
200
276
|
|
|
277
|
+
// Pattern Score (vibeScore)
|
|
201
278
|
document.getElementById('currentScore').textContent = latest ? `${latest.vibeScore}%` : '--';
|
|
202
|
-
document.getElementById('avgScore').textContent = `${stats.avgVibeScore}%`;
|
|
203
279
|
document.getElementById('achievementCount').textContent =
|
|
204
280
|
`${this.profile.achievements.length}/24`;
|
|
281
|
+
|
|
282
|
+
// Code Health Rating (metric-based quality grade)
|
|
283
|
+
const codeHealthEl = document.getElementById('codeHealthRating');
|
|
284
|
+
if (codeHealthEl && latest?.rating) {
|
|
285
|
+
codeHealthEl.textContent = latest.rating;
|
|
286
|
+
codeHealthEl.className = 'stat-value rating-badge rating-' + latest.rating.toLowerCase();
|
|
287
|
+
}
|
|
205
288
|
}
|
|
206
289
|
|
|
207
290
|
renderRecentSessions() {
|
|
@@ -218,13 +301,26 @@ class VibeDashboard {
|
|
|
218
301
|
return;
|
|
219
302
|
}
|
|
220
303
|
|
|
221
|
-
container.innerHTML = sessions.map(session =>
|
|
222
|
-
|
|
223
|
-
|
|
304
|
+
container.innerHTML = sessions.map((session, idx) => {
|
|
305
|
+
const rating = this.scoreToRating(session.vibeScore);
|
|
306
|
+
return `
|
|
307
|
+
<div class="session-item" data-session-idx="${idx}">
|
|
308
|
+
<div>
|
|
309
|
+
<div class="session-date">${this.formatDate(session.date)}</div>
|
|
310
|
+
<div class="session-commits">${session.commits} commits · ${session.spirals} spirals</div>
|
|
311
|
+
</div>
|
|
224
312
|
<div class="session-score">${session.vibeScore}%</div>
|
|
225
|
-
<span class="session-rating ${
|
|
313
|
+
<span class="session-rating ${rating.toLowerCase()}">${rating}</span>
|
|
226
314
|
</div>
|
|
227
|
-
`).join('');
|
|
315
|
+
`}).join('');
|
|
316
|
+
|
|
317
|
+
// Add click handlers
|
|
318
|
+
container.querySelectorAll('.session-item').forEach(item => {
|
|
319
|
+
item.addEventListener('click', () => {
|
|
320
|
+
const idx = parseInt(item.dataset.sessionIdx);
|
|
321
|
+
this.showSessionDetail(this.profile.sessions[idx]);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
228
324
|
}
|
|
229
325
|
|
|
230
326
|
renderInsights() {
|
|
@@ -264,18 +360,48 @@ class VibeDashboard {
|
|
|
264
360
|
}
|
|
265
361
|
|
|
266
362
|
initCharts() {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
363
|
+
// Check if Chart.js is loaded
|
|
364
|
+
if (typeof Chart === 'undefined') {
|
|
365
|
+
console.error('Chart.js not loaded');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
console.log('Initializing charts with sessions:', this.profile?.sessions?.length);
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
this.initTrendChart();
|
|
373
|
+
this.initRadarChart();
|
|
374
|
+
this.initRatingsChart();
|
|
375
|
+
console.log('All charts initialized successfully');
|
|
376
|
+
} catch (e) {
|
|
377
|
+
console.error('Error initializing charts:', e);
|
|
378
|
+
}
|
|
270
379
|
}
|
|
271
380
|
|
|
272
381
|
initTrendChart() {
|
|
273
|
-
const
|
|
274
|
-
if (!
|
|
382
|
+
const canvas = document.getElementById('trendCanvas');
|
|
383
|
+
if (!canvas) {
|
|
384
|
+
console.error('trendCanvas element not found');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
275
387
|
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
388
|
+
const ctx = canvas.getContext('2d');
|
|
389
|
+
if (!ctx) {
|
|
390
|
+
console.error('Could not get 2D context for trendCanvas');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const sessions = this.profile.sessions || [];
|
|
395
|
+
console.log('Trend chart sessions:', sessions.length, sessions.slice(0, 2));
|
|
396
|
+
if (sessions.length === 0) {
|
|
397
|
+
console.log('No sessions data for trend chart');
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const reversed = sessions.slice().reverse();
|
|
402
|
+
const labels = reversed.map(s => this.formatDate(s.date));
|
|
403
|
+
const scores = reversed.map(s => s.vibeScore);
|
|
404
|
+
console.log('Trend chart data - labels:', labels.length, 'scores:', scores.slice(0, 5));
|
|
279
405
|
|
|
280
406
|
this.charts.trend = new Chart(ctx, {
|
|
281
407
|
type: 'line',
|
|
@@ -283,7 +409,7 @@ class VibeDashboard {
|
|
|
283
409
|
labels,
|
|
284
410
|
datasets: [{
|
|
285
411
|
label: 'Vibe Score',
|
|
286
|
-
data,
|
|
412
|
+
data: scores,
|
|
287
413
|
borderColor: '#58a6ff',
|
|
288
414
|
backgroundColor: 'rgba(88, 166, 255, 0.1)',
|
|
289
415
|
fill: true,
|
|
@@ -315,17 +441,42 @@ class VibeDashboard {
|
|
|
315
441
|
}
|
|
316
442
|
|
|
317
443
|
initRadarChart() {
|
|
318
|
-
const
|
|
444
|
+
const canvas = document.getElementById('radarCanvas');
|
|
445
|
+
if (!canvas) return;
|
|
446
|
+
const ctx = canvas.getContext('2d');
|
|
319
447
|
if (!ctx) return;
|
|
320
448
|
|
|
321
|
-
//
|
|
449
|
+
// Use real metrics from dashboard data, or show defaults
|
|
450
|
+
const metrics = this.dashboardData?.charts?.avgMetrics;
|
|
451
|
+
|
|
452
|
+
// Only show real data - no fake placeholder values
|
|
453
|
+
if (!metrics) {
|
|
454
|
+
// Show empty state message instead of fake data
|
|
455
|
+
const container = canvas.parentElement;
|
|
456
|
+
container.innerHTML = `
|
|
457
|
+
<div class="empty-chart-state">
|
|
458
|
+
<span class="empty-icon">📊</span>
|
|
459
|
+
<p>Run <code>vibe-check --score</code> to see your metrics</p>
|
|
460
|
+
</div>
|
|
461
|
+
`;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const data = [
|
|
466
|
+
Math.min(100, metrics.trustPassRate),
|
|
467
|
+
Math.min(100, metrics.iterationVelocity * 10), // scale velocity (commits/hr) to 0-100
|
|
468
|
+
Math.min(100, metrics.flowEfficiency),
|
|
469
|
+
Math.max(0, 100 - metrics.reworkRatio), // invert: lower rework = higher stability
|
|
470
|
+
Math.max(0, 100 - metrics.debugSpiralDuration * 2), // scale spirals: 0 = 100, 50min = 0
|
|
471
|
+
];
|
|
472
|
+
|
|
322
473
|
this.charts.radar = new Chart(ctx, {
|
|
323
474
|
type: 'radar',
|
|
324
475
|
data: {
|
|
325
476
|
labels: ['Trust Pass', 'Velocity', 'Flow', 'Stability', 'No Spirals'],
|
|
326
477
|
datasets: [{
|
|
327
|
-
label: '
|
|
328
|
-
data:
|
|
478
|
+
label: 'Average (Last 10)',
|
|
479
|
+
data: data,
|
|
329
480
|
borderColor: '#58a6ff',
|
|
330
481
|
backgroundColor: 'rgba(88, 166, 255, 0.2)',
|
|
331
482
|
pointBackgroundColor: '#58a6ff',
|
|
@@ -352,7 +503,9 @@ class VibeDashboard {
|
|
|
352
503
|
}
|
|
353
504
|
|
|
354
505
|
initRatingsChart() {
|
|
355
|
-
const
|
|
506
|
+
const canvas = document.getElementById('ratingsCanvas');
|
|
507
|
+
if (!canvas) return;
|
|
508
|
+
const ctx = canvas.getContext('2d');
|
|
356
509
|
if (!ctx) return;
|
|
357
510
|
|
|
358
511
|
// Count ratings
|
|
@@ -444,25 +597,198 @@ class VibeDashboard {
|
|
|
444
597
|
|
|
445
598
|
renderHistory() {
|
|
446
599
|
const container = document.getElementById('historyContainer');
|
|
600
|
+
let sessions = [...this.profile.sessions];
|
|
601
|
+
|
|
602
|
+
// Apply filters
|
|
603
|
+
const { rating, sortBy, range } = this.historyFilters;
|
|
604
|
+
|
|
605
|
+
// Rating filter
|
|
606
|
+
if (rating !== 'all') {
|
|
607
|
+
const ratingOrder = ['ELITE', 'HIGH', 'MEDIUM', 'LOW'];
|
|
608
|
+
const minIdx = ratingOrder.indexOf(rating);
|
|
609
|
+
sessions = sessions.filter(s => ratingOrder.indexOf(s.overall) <= minIdx);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Time range filter
|
|
613
|
+
if (range !== 'all') {
|
|
614
|
+
const daysAgo = parseInt(range);
|
|
615
|
+
const cutoff = new Date();
|
|
616
|
+
cutoff.setDate(cutoff.getDate() - daysAgo);
|
|
617
|
+
sessions = sessions.filter(s => new Date(s.date) >= cutoff);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Sorting
|
|
621
|
+
sessions.sort((a, b) => {
|
|
622
|
+
switch (sortBy) {
|
|
623
|
+
case 'date-asc': return new Date(a.date) - new Date(b.date);
|
|
624
|
+
case 'date-desc': return new Date(b.date) - new Date(a.date);
|
|
625
|
+
case 'score-desc': return b.vibeScore - a.vibeScore;
|
|
626
|
+
case 'score-asc': return a.vibeScore - b.vibeScore;
|
|
627
|
+
case 'commits-desc': return b.commits - a.commits;
|
|
628
|
+
default: return 0;
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Update stats
|
|
633
|
+
const avgScore = sessions.length > 0
|
|
634
|
+
? Math.round(sessions.reduce((sum, s) => sum + s.vibeScore, 0) / sessions.length)
|
|
635
|
+
: 0;
|
|
636
|
+
document.getElementById('historyCount').textContent = `${sessions.length} sessions`;
|
|
637
|
+
document.getElementById('historyAvg').textContent = `Avg: ${avgScore}%`;
|
|
638
|
+
|
|
639
|
+
// Render heatmap
|
|
640
|
+
this.renderHeatmap();
|
|
641
|
+
|
|
642
|
+
// Render scope health chart
|
|
643
|
+
this.renderScopeHealthChart();
|
|
447
644
|
|
|
645
|
+
// Render session list
|
|
448
646
|
container.innerHTML = `
|
|
449
|
-
<div class="
|
|
450
|
-
|
|
451
|
-
<
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
647
|
+
<div class="recent-section">
|
|
648
|
+
<div class="section-header">
|
|
649
|
+
<h3>All Sessions</h3>
|
|
650
|
+
</div>
|
|
651
|
+
<div class="sessions-list">
|
|
652
|
+
${sessions.length === 0 ? `
|
|
653
|
+
<div class="empty-state">
|
|
654
|
+
<span class="empty-icon">🔍</span>
|
|
655
|
+
<p>No sessions match your filters</p>
|
|
457
656
|
</div>
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
657
|
+
` : sessions.map((session, idx) => {
|
|
658
|
+
const trend = this.getSessionTrend(session, idx, sessions);
|
|
659
|
+
const rating = this.scoreToRating(session.vibeScore);
|
|
660
|
+
return `
|
|
661
|
+
<div class="session-item" data-session-idx="${this.profile.sessions.indexOf(session)}">
|
|
662
|
+
<div>
|
|
663
|
+
<div class="session-date">${this.formatDateLong(session.date)}</div>
|
|
664
|
+
<div class="session-commits">${session.commits} commits · ${session.spirals} spirals · +${session.xpEarned || 0} XP</div>
|
|
665
|
+
</div>
|
|
666
|
+
${trend}
|
|
667
|
+
<div class="session-score">${session.vibeScore}%</div>
|
|
668
|
+
<span class="session-rating ${rating.toLowerCase()}">${rating}</span>
|
|
669
|
+
</div>
|
|
670
|
+
`;
|
|
671
|
+
}).join('')}
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
`;
|
|
675
|
+
|
|
676
|
+
// Add click handlers
|
|
677
|
+
container.querySelectorAll('.session-item').forEach(item => {
|
|
678
|
+
item.addEventListener('click', () => {
|
|
679
|
+
const idx = parseInt(item.dataset.sessionIdx);
|
|
680
|
+
this.showSessionDetail(this.profile.sessions[idx]);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
getSessionTrend(session, idx, sessions) {
|
|
686
|
+
if (idx >= sessions.length - 1) return '<span class="trend-indicator neutral">—</span>';
|
|
687
|
+
const prev = sessions[idx + 1];
|
|
688
|
+
const diff = session.vibeScore - prev.vibeScore;
|
|
689
|
+
if (diff > 5) return `<span class="trend-indicator up">↑ +${diff}</span>`;
|
|
690
|
+
if (diff < -5) return `<span class="trend-indicator down">↓ ${diff}</span>`;
|
|
691
|
+
return '<span class="trend-indicator neutral">→</span>';
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
renderHeatmap() {
|
|
695
|
+
const container = document.getElementById('heatmapContainer');
|
|
696
|
+
if (!container || !this.dashboardData?.charts?.hourlyActivity) return;
|
|
697
|
+
|
|
698
|
+
const activity = this.dashboardData.charts.hourlyActivity;
|
|
699
|
+
const maxActivity = Math.max(...Object.values(activity), 1);
|
|
700
|
+
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
701
|
+
|
|
702
|
+
// Build hour headers (0-23 simplified to key hours)
|
|
703
|
+
let hourLabels = '';
|
|
704
|
+
for (let h = 0; h < 24; h += 3) {
|
|
705
|
+
hourLabels += `<div class="heatmap-hour" style="grid-column: span 3">${h}:00</div>`;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Build grid (simplified - just show hourly data as a single row)
|
|
709
|
+
let cells = '';
|
|
710
|
+
for (let h = 0; h < 24; h++) {
|
|
711
|
+
const count = activity[h] || 0;
|
|
712
|
+
const level = Math.ceil((count / maxActivity) * 5);
|
|
713
|
+
cells += `<div class="heatmap-cell level-${level}" title="${count} commits at ${h}:00"></div>`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
container.innerHTML = `
|
|
717
|
+
<div class="heatmap-grid" style="grid-template-columns: 50px repeat(24, 1fr);">
|
|
718
|
+
<div class="heatmap-label">Hours</div>
|
|
719
|
+
${Array.from({length: 24}, (_, h) =>
|
|
720
|
+
`<div class="heatmap-hour">${h}</div>`
|
|
721
|
+
).join('')}
|
|
722
|
+
<div class="heatmap-label">Activity</div>
|
|
723
|
+
${cells}
|
|
724
|
+
</div>
|
|
725
|
+
<div class="heatmap-legend">
|
|
726
|
+
<span>Less</span>
|
|
727
|
+
${[0,1,2,3,4,5].map(l => `<div class="heatmap-legend-cell level-${l}"></div>`).join('')}
|
|
728
|
+
<span>More</span>
|
|
462
729
|
</div>
|
|
463
730
|
`;
|
|
464
731
|
}
|
|
465
732
|
|
|
733
|
+
renderScopeHealthChart() {
|
|
734
|
+
const canvas = document.getElementById('scopeHealthCanvas');
|
|
735
|
+
if (!canvas || !this.dashboardData?.charts?.scopeHealth) return;
|
|
736
|
+
|
|
737
|
+
// Destroy existing chart
|
|
738
|
+
if (this.charts.scopeHealth) {
|
|
739
|
+
this.charts.scopeHealth.destroy();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const ctx = canvas.getContext('2d');
|
|
743
|
+
if (!ctx) return;
|
|
744
|
+
|
|
745
|
+
const scopeData = this.dashboardData.charts.scopeHealth.slice(0, 8);
|
|
746
|
+
const labels = scopeData.map(s => s.scope || '(none)');
|
|
747
|
+
const commits = scopeData.map(s => s.commits);
|
|
748
|
+
const fixRatios = scopeData.map(s => s.fixRatio);
|
|
749
|
+
|
|
750
|
+
this.charts.scopeHealth = new Chart(ctx, {
|
|
751
|
+
type: 'bar',
|
|
752
|
+
data: {
|
|
753
|
+
labels,
|
|
754
|
+
datasets: [
|
|
755
|
+
{
|
|
756
|
+
label: 'Commits',
|
|
757
|
+
data: commits,
|
|
758
|
+
backgroundColor: '#58a6ff',
|
|
759
|
+
borderRadius: 4,
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
label: 'Fix Ratio %',
|
|
763
|
+
data: fixRatios,
|
|
764
|
+
backgroundColor: '#f85149',
|
|
765
|
+
borderRadius: 4,
|
|
766
|
+
}
|
|
767
|
+
]
|
|
768
|
+
},
|
|
769
|
+
options: {
|
|
770
|
+
responsive: true,
|
|
771
|
+
maintainAspectRatio: false,
|
|
772
|
+
plugins: {
|
|
773
|
+
legend: {
|
|
774
|
+
position: 'bottom',
|
|
775
|
+
labels: { color: '#7d8590' }
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
scales: {
|
|
779
|
+
x: {
|
|
780
|
+
grid: { display: false },
|
|
781
|
+
ticks: { color: '#7d8590' }
|
|
782
|
+
},
|
|
783
|
+
y: {
|
|
784
|
+
grid: { color: '#30363d' },
|
|
785
|
+
ticks: { color: '#7d8590' }
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
466
792
|
renderProfile() {
|
|
467
793
|
const container = document.getElementById('profileContainer');
|
|
468
794
|
const { stats, xp, streak } = this.profile;
|
|
@@ -544,6 +870,293 @@ class VibeDashboard {
|
|
|
544
870
|
document.getElementById('achievementModal').classList.remove('show');
|
|
545
871
|
}
|
|
546
872
|
|
|
873
|
+
closeSessionModal() {
|
|
874
|
+
document.getElementById('sessionModal').classList.remove('show');
|
|
875
|
+
// Destroy detail chart to free memory
|
|
876
|
+
if (this.charts.detailRadar) {
|
|
877
|
+
this.charts.detailRadar.destroy();
|
|
878
|
+
this.charts.detailRadar = null;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
closeMetricsModal() {
|
|
883
|
+
document.getElementById('metricsModal')?.classList.remove('show');
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
showMetricsHelp(type) {
|
|
887
|
+
const modal = document.getElementById('metricsModal');
|
|
888
|
+
const title = document.getElementById('metricsModalTitle');
|
|
889
|
+
const content = document.getElementById('metricsHelpContent');
|
|
890
|
+
if (!modal || !content) return;
|
|
891
|
+
|
|
892
|
+
if (type === 'codeHealth') {
|
|
893
|
+
title.textContent = 'Code Health Rating';
|
|
894
|
+
content.innerHTML = `
|
|
895
|
+
<div class="metrics-help-section">
|
|
896
|
+
<p class="metrics-intro">Code Health grades your <strong>actual coding outcomes</strong>. It's calculated from 5 metrics that measure whether your code works on first try.</p>
|
|
897
|
+
|
|
898
|
+
<h3>The 5 Quality Metrics</h3>
|
|
899
|
+
|
|
900
|
+
<div class="metric-card">
|
|
901
|
+
<div class="metric-header">
|
|
902
|
+
<span class="metric-name">Iteration Velocity</span>
|
|
903
|
+
<span class="metric-formula">commits / active hours</span>
|
|
904
|
+
</div>
|
|
905
|
+
<p>How fast you're committing. Tight feedback loops catch issues early.</p>
|
|
906
|
+
<div class="metric-thresholds">
|
|
907
|
+
<span class="threshold elite">ELITE: >5/hr</span>
|
|
908
|
+
<span class="threshold high">HIGH: >=3/hr</span>
|
|
909
|
+
<span class="threshold medium">MEDIUM: >=1/hr</span>
|
|
910
|
+
<span class="threshold low">LOW: <1/hr</span>
|
|
911
|
+
</div>
|
|
912
|
+
</div>
|
|
913
|
+
|
|
914
|
+
<div class="metric-card">
|
|
915
|
+
<div class="metric-header">
|
|
916
|
+
<span class="metric-name">Rework Ratio</span>
|
|
917
|
+
<span class="metric-formula">fix commits / total commits</span>
|
|
918
|
+
</div>
|
|
919
|
+
<p>How much time you spend debugging vs building. Lower is better.</p>
|
|
920
|
+
<div class="metric-thresholds">
|
|
921
|
+
<span class="threshold elite">ELITE: <30%</span>
|
|
922
|
+
<span class="threshold high">HIGH: <50%</span>
|
|
923
|
+
<span class="threshold medium">MEDIUM: <70%</span>
|
|
924
|
+
<span class="threshold low">LOW: >=70%</span>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
|
|
928
|
+
<div class="metric-card">
|
|
929
|
+
<div class="metric-header">
|
|
930
|
+
<span class="metric-name">Trust Pass Rate</span>
|
|
931
|
+
<span class="metric-formula">commits without immediate fix / total</span>
|
|
932
|
+
</div>
|
|
933
|
+
<p>Does your code stick on first try? A commit "fails" if followed by a fix within 30min.</p>
|
|
934
|
+
<div class="metric-thresholds">
|
|
935
|
+
<span class="threshold elite">ELITE: >95%</span>
|
|
936
|
+
<span class="threshold high">HIGH: >=80%</span>
|
|
937
|
+
<span class="threshold medium">MEDIUM: >=60%</span>
|
|
938
|
+
<span class="threshold low">LOW: <60%</span>
|
|
939
|
+
</div>
|
|
940
|
+
</div>
|
|
941
|
+
|
|
942
|
+
<div class="metric-card">
|
|
943
|
+
<div class="metric-header">
|
|
944
|
+
<span class="metric-name">Debug Spiral Duration</span>
|
|
945
|
+
<span class="metric-formula">avg minutes in 3+ consecutive fix chains</span>
|
|
946
|
+
</div>
|
|
947
|
+
<p>How long you stay stuck when debugging. Spirals = 3+ fixes to same component.</p>
|
|
948
|
+
<div class="metric-thresholds">
|
|
949
|
+
<span class="threshold elite">ELITE: <15m</span>
|
|
950
|
+
<span class="threshold high">HIGH: <30m</span>
|
|
951
|
+
<span class="threshold medium">MEDIUM: <60m</span>
|
|
952
|
+
<span class="threshold low">LOW: >=60m</span>
|
|
953
|
+
</div>
|
|
954
|
+
</div>
|
|
955
|
+
|
|
956
|
+
<div class="metric-card">
|
|
957
|
+
<div class="metric-header">
|
|
958
|
+
<span class="metric-name">Flow Efficiency</span>
|
|
959
|
+
<span class="metric-formula">(active time - spiral time) / active time</span>
|
|
960
|
+
</div>
|
|
961
|
+
<p>Percentage of time spent building vs stuck debugging.</p>
|
|
962
|
+
<div class="metric-thresholds">
|
|
963
|
+
<span class="threshold elite">ELITE: >90%</span>
|
|
964
|
+
<span class="threshold high">HIGH: >=75%</span>
|
|
965
|
+
<span class="threshold medium">MEDIUM: >=50%</span>
|
|
966
|
+
<span class="threshold low">LOW: <50%</span>
|
|
967
|
+
</div>
|
|
968
|
+
</div>
|
|
969
|
+
|
|
970
|
+
<h3>How It's Calculated</h3>
|
|
971
|
+
<p>Each metric gets rated (ELITE=4, HIGH=3, MEDIUM=2, LOW=1). Your Code Health is the average:</p>
|
|
972
|
+
<div class="formula-box">
|
|
973
|
+
avg >= 3.5 → ELITE | avg >= 2.5 → HIGH | avg >= 1.5 → MEDIUM | else → LOW
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
`;
|
|
977
|
+
} else if (type === 'patternScore') {
|
|
978
|
+
title.textContent = 'Pattern Score';
|
|
979
|
+
content.innerHTML = `
|
|
980
|
+
<div class="metrics-help-section">
|
|
981
|
+
<p class="metrics-intro">Pattern Score detects <strong>workflow problems</strong> even without conventional commit messages. It's an early warning system for trouble.</p>
|
|
982
|
+
|
|
983
|
+
<h3>The 4 Pattern Metrics</h3>
|
|
984
|
+
|
|
985
|
+
<div class="metric-card">
|
|
986
|
+
<div class="metric-header">
|
|
987
|
+
<span class="metric-name">File Churn</span>
|
|
988
|
+
<span class="metric-weight">30% weight</span>
|
|
989
|
+
</div>
|
|
990
|
+
<p>Files touched 3+ times in 1 hour. High churn suggests thrashing or incomplete understanding.</p>
|
|
991
|
+
<div class="metric-formula">score = (1 - churned_files / total_files) × 100</div>
|
|
992
|
+
<div class="metric-thresholds">
|
|
993
|
+
<span class="threshold elite">ELITE: >90%</span>
|
|
994
|
+
<span class="threshold high">HIGH: 75-90%</span>
|
|
995
|
+
<span class="threshold medium">MEDIUM: 60-75%</span>
|
|
996
|
+
<span class="threshold low">LOW: <60%</span>
|
|
997
|
+
</div>
|
|
998
|
+
</div>
|
|
999
|
+
|
|
1000
|
+
<div class="metric-card">
|
|
1001
|
+
<div class="metric-header">
|
|
1002
|
+
<span class="metric-name">Time Spiral</span>
|
|
1003
|
+
<span class="metric-weight">25% weight</span>
|
|
1004
|
+
</div>
|
|
1005
|
+
<p>Commits less than 5 minutes apart. Rapid commits suggest frustrated iteration.</p>
|
|
1006
|
+
<div class="metric-formula">score = (1 - rapid_commits / total_commits) × 100</div>
|
|
1007
|
+
<div class="metric-thresholds">
|
|
1008
|
+
<span class="threshold elite">ELITE: >85%</span>
|
|
1009
|
+
<span class="threshold high">HIGH: 70-85%</span>
|
|
1010
|
+
<span class="threshold medium">MEDIUM: 50-70%</span>
|
|
1011
|
+
<span class="threshold low">LOW: <50%</span>
|
|
1012
|
+
</div>
|
|
1013
|
+
</div>
|
|
1014
|
+
|
|
1015
|
+
<div class="metric-card">
|
|
1016
|
+
<div class="metric-header">
|
|
1017
|
+
<span class="metric-name">Velocity Anomaly</span>
|
|
1018
|
+
<span class="metric-weight">20% weight</span>
|
|
1019
|
+
</div>
|
|
1020
|
+
<p>How far from your personal baseline. Unusual velocity (too fast/slow) signals problems.</p>
|
|
1021
|
+
<div class="metric-formula">z-score = |velocity - baseline| / stdDev</div>
|
|
1022
|
+
<div class="metric-thresholds">
|
|
1023
|
+
<span class="threshold elite">ELITE: <1σ</span>
|
|
1024
|
+
<span class="threshold high">HIGH: <1.5σ</span>
|
|
1025
|
+
<span class="threshold medium">MEDIUM: <2σ</span>
|
|
1026
|
+
<span class="threshold low">LOW: >=2σ</span>
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
|
|
1030
|
+
<div class="metric-card">
|
|
1031
|
+
<div class="metric-header">
|
|
1032
|
+
<span class="metric-name">Code Stability</span>
|
|
1033
|
+
<span class="metric-weight">25% weight</span>
|
|
1034
|
+
</div>
|
|
1035
|
+
<p>What percentage of added code survives. High churn = building on wrong assumptions.</p>
|
|
1036
|
+
<div class="metric-formula">score = (1 - deletions/additions × 0.5) × 100</div>
|
|
1037
|
+
<div class="metric-thresholds">
|
|
1038
|
+
<span class="threshold elite">ELITE: >=85%</span>
|
|
1039
|
+
<span class="threshold high">HIGH: >=70%</span>
|
|
1040
|
+
<span class="threshold medium">MEDIUM: >=50%</span>
|
|
1041
|
+
<span class="threshold low">LOW: <50%</span>
|
|
1042
|
+
</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
|
|
1045
|
+
<h3>How It's Calculated</h3>
|
|
1046
|
+
<p>Weighted sum of all pattern metrics:</p>
|
|
1047
|
+
<div class="formula-box">
|
|
1048
|
+
Pattern Score = (fileChurn × 0.30) + (timeSpiral × 0.25) + (velocityAnomaly × 0.20) + (codeStability × 0.25)
|
|
1049
|
+
</div>
|
|
1050
|
+
</div>
|
|
1051
|
+
`;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
modal.classList.add('show');
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Derive rating from vibeScore to ensure consistency
|
|
1058
|
+
scoreToRating(score) {
|
|
1059
|
+
if (score >= 85) return 'ELITE';
|
|
1060
|
+
if (score >= 70) return 'HIGH';
|
|
1061
|
+
if (score >= 50) return 'MEDIUM';
|
|
1062
|
+
return 'LOW';
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
showSessionDetail(session) {
|
|
1066
|
+
if (!session) return;
|
|
1067
|
+
|
|
1068
|
+
// Update modal content
|
|
1069
|
+
const scoreRing = document.querySelector('.detail-score-ring');
|
|
1070
|
+
scoreRing.style.setProperty('--score', session.vibeScore);
|
|
1071
|
+
|
|
1072
|
+
// Calculate rating from score to ensure badge matches displayed percentage
|
|
1073
|
+
const rating = this.scoreToRating(session.vibeScore);
|
|
1074
|
+
document.getElementById('detailScore').textContent = `${session.vibeScore}%`;
|
|
1075
|
+
document.getElementById('detailRating').textContent = rating;
|
|
1076
|
+
document.getElementById('detailDate').textContent = this.formatDateLong(session.date);
|
|
1077
|
+
document.getElementById('detailXP').textContent = session.xpEarned || 0;
|
|
1078
|
+
document.getElementById('detailCommits').textContent = session.commits;
|
|
1079
|
+
document.getElementById('detailSpirals').textContent = session.spirals;
|
|
1080
|
+
|
|
1081
|
+
// Use real metrics if available, otherwise estimate
|
|
1082
|
+
const metrics = session.metrics;
|
|
1083
|
+
const velocity = metrics?.iterationVelocity?.toFixed(1) || `~${Math.round(session.commits / 2)}`;
|
|
1084
|
+
const trustRate = metrics?.trustPassRate?.toFixed(0) || Math.max(0, 100 - (session.spirals * 10));
|
|
1085
|
+
document.getElementById('detailVelocity').textContent = `${velocity}/hr`;
|
|
1086
|
+
document.getElementById('detailTrust').textContent = `${trustRate}%`;
|
|
1087
|
+
|
|
1088
|
+
// Render detail radar chart
|
|
1089
|
+
this.renderDetailRadar(session);
|
|
1090
|
+
|
|
1091
|
+
// Show modal
|
|
1092
|
+
document.getElementById('sessionModal').classList.add('show');
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
renderDetailRadar(session) {
|
|
1096
|
+
const canvas = document.getElementById('detailRadarCanvas');
|
|
1097
|
+
if (!canvas) return;
|
|
1098
|
+
|
|
1099
|
+
// Destroy existing chart
|
|
1100
|
+
if (this.charts.detailRadar) {
|
|
1101
|
+
this.charts.detailRadar.destroy();
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const ctx = canvas.getContext('2d');
|
|
1105
|
+
if (!ctx) return;
|
|
1106
|
+
|
|
1107
|
+
// Use real metrics if available, otherwise estimate from session data
|
|
1108
|
+
const m = session.metrics;
|
|
1109
|
+
let data;
|
|
1110
|
+
if (m) {
|
|
1111
|
+
data = [
|
|
1112
|
+
Math.min(100, m.trustPassRate),
|
|
1113
|
+
Math.min(100, m.iterationVelocity * 10),
|
|
1114
|
+
Math.min(100, m.flowEfficiency),
|
|
1115
|
+
Math.max(0, 100 - m.reworkRatio),
|
|
1116
|
+
Math.max(0, 100 - m.debugSpiralDuration * 2),
|
|
1117
|
+
];
|
|
1118
|
+
} else {
|
|
1119
|
+
// Fallback to estimates
|
|
1120
|
+
const trustPass = Math.max(0, 100 - (session.spirals * 15));
|
|
1121
|
+
const velocity = Math.min(100, (session.commits / 50) * 100);
|
|
1122
|
+
const flow = session.vibeScore;
|
|
1123
|
+
const stability = session.spirals === 0 ? 100 : Math.max(0, 100 - (session.spirals * 20));
|
|
1124
|
+
const noSpirals = session.spirals === 0 ? 100 : Math.max(0, 100 - (session.spirals * 25));
|
|
1125
|
+
data = [trustPass, velocity, flow, stability, noSpirals];
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
this.charts.detailRadar = new Chart(ctx, {
|
|
1129
|
+
type: 'radar',
|
|
1130
|
+
data: {
|
|
1131
|
+
labels: ['Trust Pass', 'Velocity', 'Flow', 'Stability', 'No Spirals'],
|
|
1132
|
+
datasets: [{
|
|
1133
|
+
label: m ? 'Session Metrics' : 'Estimated',
|
|
1134
|
+
data: data,
|
|
1135
|
+
borderColor: '#58a6ff',
|
|
1136
|
+
backgroundColor: 'rgba(88, 166, 255, 0.2)',
|
|
1137
|
+
pointBackgroundColor: '#58a6ff',
|
|
1138
|
+
}]
|
|
1139
|
+
},
|
|
1140
|
+
options: {
|
|
1141
|
+
responsive: true,
|
|
1142
|
+
maintainAspectRatio: true,
|
|
1143
|
+
plugins: {
|
|
1144
|
+
legend: { display: false },
|
|
1145
|
+
},
|
|
1146
|
+
scales: {
|
|
1147
|
+
r: {
|
|
1148
|
+
min: 0,
|
|
1149
|
+
max: 100,
|
|
1150
|
+
grid: { color: '#30363d' },
|
|
1151
|
+
angleLines: { color: '#30363d' },
|
|
1152
|
+
pointLabels: { color: '#7d8590', font: { size: 10 } },
|
|
1153
|
+
ticks: { display: false },
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
547
1160
|
showAchievementModal(achievement) {
|
|
548
1161
|
document.getElementById('modalAchievementIcon').textContent = achievement.icon;
|
|
549
1162
|
document.getElementById('modalAchievementName').textContent = achievement.name;
|
|
@@ -560,8 +1173,28 @@ class VibeDashboard {
|
|
|
560
1173
|
const date = new Date(dateStr);
|
|
561
1174
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
562
1175
|
}
|
|
1176
|
+
|
|
1177
|
+
formatDateLong(dateStr) {
|
|
1178
|
+
const date = new Date(dateStr);
|
|
1179
|
+
return date.toLocaleDateString('en-US', {
|
|
1180
|
+
weekday: 'short',
|
|
1181
|
+
month: 'short',
|
|
1182
|
+
day: 'numeric',
|
|
1183
|
+
hour: 'numeric',
|
|
1184
|
+
minute: '2-digit'
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
563
1187
|
}
|
|
564
1188
|
|
|
565
1189
|
// Initialize dashboard
|
|
566
1190
|
const dashboard = new VibeDashboard();
|
|
567
|
-
document.addEventListener('DOMContentLoaded', () =>
|
|
1191
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1192
|
+
dashboard.init();
|
|
1193
|
+
// Retry chart init after a delay in case Chart.js loaded late
|
|
1194
|
+
if (!dashboard.charts.trend) {
|
|
1195
|
+
setTimeout(() => {
|
|
1196
|
+
console.log('Retrying chart initialization...');
|
|
1197
|
+
dashboard.initCharts();
|
|
1198
|
+
}, 500);
|
|
1199
|
+
}
|
|
1200
|
+
});
|