elspais 0.9.3__py3-none-any.whl → 0.11.1__py3-none-any.whl

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.
Files changed (73) hide show
  1. elspais/cli.py +141 -10
  2. elspais/commands/hash_cmd.py +72 -26
  3. elspais/commands/reformat_cmd.py +458 -0
  4. elspais/commands/trace.py +157 -3
  5. elspais/commands/validate.py +44 -16
  6. elspais/core/models.py +2 -0
  7. elspais/core/parser.py +68 -24
  8. elspais/reformat/__init__.py +50 -0
  9. elspais/reformat/detector.py +119 -0
  10. elspais/reformat/hierarchy.py +246 -0
  11. elspais/reformat/line_breaks.py +220 -0
  12. elspais/reformat/prompts.py +123 -0
  13. elspais/reformat/transformer.py +264 -0
  14. elspais/sponsors/__init__.py +432 -0
  15. elspais/trace_view/__init__.py +54 -0
  16. elspais/trace_view/coverage.py +183 -0
  17. elspais/trace_view/generators/__init__.py +12 -0
  18. elspais/trace_view/generators/base.py +329 -0
  19. elspais/trace_view/generators/csv.py +122 -0
  20. elspais/trace_view/generators/markdown.py +175 -0
  21. elspais/trace_view/html/__init__.py +31 -0
  22. elspais/trace_view/html/generator.py +1006 -0
  23. elspais/trace_view/html/templates/base.html +283 -0
  24. elspais/trace_view/html/templates/components/code_viewer_modal.html +14 -0
  25. elspais/trace_view/html/templates/components/file_picker_modal.html +20 -0
  26. elspais/trace_view/html/templates/components/legend_modal.html +69 -0
  27. elspais/trace_view/html/templates/components/review_panel.html +118 -0
  28. elspais/trace_view/html/templates/partials/review/help/help-panel.json +244 -0
  29. elspais/trace_view/html/templates/partials/review/help/onboarding.json +77 -0
  30. elspais/trace_view/html/templates/partials/review/help/tooltips.json +237 -0
  31. elspais/trace_view/html/templates/partials/review/review-comments.js +928 -0
  32. elspais/trace_view/html/templates/partials/review/review-data.js +961 -0
  33. elspais/trace_view/html/templates/partials/review/review-help.js +679 -0
  34. elspais/trace_view/html/templates/partials/review/review-init.js +177 -0
  35. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +429 -0
  36. elspais/trace_view/html/templates/partials/review/review-packages.js +1029 -0
  37. elspais/trace_view/html/templates/partials/review/review-position.js +540 -0
  38. elspais/trace_view/html/templates/partials/review/review-resize.js +115 -0
  39. elspais/trace_view/html/templates/partials/review/review-status.js +659 -0
  40. elspais/trace_view/html/templates/partials/review/review-sync.js +992 -0
  41. elspais/trace_view/html/templates/partials/review-styles.css +2238 -0
  42. elspais/trace_view/html/templates/partials/scripts.js +1741 -0
  43. elspais/trace_view/html/templates/partials/styles.css +1756 -0
  44. elspais/trace_view/models.py +353 -0
  45. elspais/trace_view/review/__init__.py +60 -0
  46. elspais/trace_view/review/branches.py +1149 -0
  47. elspais/trace_view/review/models.py +1205 -0
  48. elspais/trace_view/review/position.py +609 -0
  49. elspais/trace_view/review/server.py +1056 -0
  50. elspais/trace_view/review/status.py +470 -0
  51. elspais/trace_view/review/storage.py +1367 -0
  52. elspais/trace_view/scanning.py +213 -0
  53. elspais/trace_view/specs/README.md +84 -0
  54. elspais/trace_view/specs/tv-d00001-template-architecture.md +36 -0
  55. elspais/trace_view/specs/tv-d00002-css-extraction.md +37 -0
  56. elspais/trace_view/specs/tv-d00003-js-extraction.md +43 -0
  57. elspais/trace_view/specs/tv-d00004-build-embedding.md +40 -0
  58. elspais/trace_view/specs/tv-d00005-test-format.md +78 -0
  59. elspais/trace_view/specs/tv-d00010-review-data-models.md +33 -0
  60. elspais/trace_view/specs/tv-d00011-review-storage.md +33 -0
  61. elspais/trace_view/specs/tv-d00012-position-resolution.md +33 -0
  62. elspais/trace_view/specs/tv-d00013-git-branches.md +31 -0
  63. elspais/trace_view/specs/tv-d00014-review-api-server.md +31 -0
  64. elspais/trace_view/specs/tv-d00015-status-modifier.md +27 -0
  65. elspais/trace_view/specs/tv-d00016-js-integration.md +33 -0
  66. elspais/trace_view/specs/tv-p00001-html-generator.md +33 -0
  67. elspais/trace_view/specs/tv-p00002-review-system.md +29 -0
  68. {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/METADATA +36 -18
  69. elspais-0.11.1.dist-info/RECORD +101 -0
  70. elspais-0.9.3.dist-info/RECORD +0 -40
  71. {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/WHEEL +0 -0
  72. {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/entry_points.txt +0 -0
  73. {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,992 @@
1
+ /**
2
+ * TraceView Review Sync & Fetch Module
3
+ *
4
+ * Handles synchronization of review data:
5
+ * - Fetch review data from server/CLI
6
+ * - Push changes to server
7
+ * - Conflict handling UI
8
+ * - Refresh button logic
9
+ *
10
+ * IMPLEMENTS REQUIREMENTS:
11
+ * REQ-tv-d00016: Review JavaScript Integration
12
+ */
13
+
14
+ // Ensure TraceView.review namespace exists
15
+ window.TraceView = window.TraceView || {};
16
+ TraceView.review = TraceView.review || {};
17
+
18
+ (function(review) {
19
+ 'use strict';
20
+
21
+ // ==========================================================================
22
+ // Configuration
23
+ // ==========================================================================
24
+
25
+ review.syncConfig = {
26
+ apiEndpoint: '/api/reviews', // Base endpoint for review API
27
+ autoFetchInterval: 60000, // Auto-fetch every 60 seconds
28
+ retryAttempts: 3,
29
+ retryDelay: 1000
30
+ };
31
+
32
+ // Sync state
33
+ let isSyncing = false;
34
+ let lastSyncTime = null;
35
+ let autoFetchTimer = null;
36
+
37
+ // ==========================================================================
38
+ // Fetch Operations
39
+ // ==========================================================================
40
+
41
+ /**
42
+ * Fetch all review data from server
43
+ * @param {Object} options - Fetch options
44
+ * @returns {Promise<Object>} Review data
45
+ */
46
+ async function fetchReviewData(options = {}) {
47
+ if (isSyncing) {
48
+ console.warn('Sync already in progress');
49
+ return null;
50
+ }
51
+
52
+ isSyncing = true;
53
+ showSyncIndicator('Fetching...');
54
+
55
+ try {
56
+ const users = options.users || [];
57
+ const queryParams = new URLSearchParams();
58
+ if (users.length > 0) {
59
+ queryParams.set('users', users.join(','));
60
+ }
61
+
62
+ const url = `${review.syncConfig.apiEndpoint}?${queryParams}`;
63
+ const response = await fetchWithRetry(url, {
64
+ method: 'GET',
65
+ headers: {
66
+ 'Accept': 'application/json'
67
+ }
68
+ });
69
+
70
+ if (!response.ok) {
71
+ throw new Error(`Fetch failed: ${response.status}`);
72
+ }
73
+
74
+ const data = await response.json();
75
+
76
+ // Load data into state
77
+ review.state.loadFromEmbedded(data);
78
+ lastSyncTime = new Date();
79
+
80
+ // Trigger refresh event
81
+ document.dispatchEvent(new CustomEvent('traceview:data-fetched', {
82
+ detail: { data, timestamp: lastSyncTime }
83
+ }));
84
+
85
+ showSyncIndicator('Synced', 'success');
86
+ return data;
87
+
88
+ } catch (error) {
89
+ console.error('Fetch error:', error);
90
+ showSyncIndicator('Sync failed', 'error');
91
+ throw error;
92
+ } finally {
93
+ isSyncing = false;
94
+ }
95
+ }
96
+ review.fetchReviewData = fetchReviewData;
97
+
98
+ /**
99
+ * Fetch review data for a specific requirement
100
+ * @param {string} reqId - Requirement ID
101
+ * @returns {Promise<Object>} Review data for requirement
102
+ */
103
+ async function fetchReqReviewData(reqId) {
104
+ showSyncIndicator('Fetching...');
105
+
106
+ try {
107
+ const url = `${review.syncConfig.apiEndpoint}/reqs/${review.normalizeReqId(reqId)}`;
108
+ const response = await fetchWithRetry(url, {
109
+ method: 'GET',
110
+ headers: {
111
+ 'Accept': 'application/json'
112
+ }
113
+ });
114
+
115
+ if (!response.ok) {
116
+ if (response.status === 404) {
117
+ return { threads: [], requests: [], flag: null };
118
+ }
119
+ throw new Error(`Fetch failed: ${response.status}`);
120
+ }
121
+
122
+ const data = await response.json();
123
+ showSyncIndicator('Synced', 'success');
124
+ return data;
125
+
126
+ } catch (error) {
127
+ console.error('Fetch error:', error);
128
+ showSyncIndicator('Sync failed', 'error');
129
+ throw error;
130
+ }
131
+ }
132
+ review.fetchReqReviewData = fetchReqReviewData;
133
+
134
+ // ==========================================================================
135
+ // Push Operations
136
+ // ==========================================================================
137
+
138
+ /**
139
+ * Push a new thread to server
140
+ * @param {Thread} thread - Thread to push
141
+ * @returns {Promise<Object>} Response data
142
+ */
143
+ async function pushThread(thread) {
144
+ if (!review.state.config.pushOnComment) {
145
+ console.log('Push on comment disabled');
146
+ return null;
147
+ }
148
+
149
+ showSyncIndicator('Saving...');
150
+
151
+ try {
152
+ const url = `${review.syncConfig.apiEndpoint}/reqs/${review.normalizeReqId(thread.reqId)}/threads`;
153
+ const response = await fetchWithRetry(url, {
154
+ method: 'POST',
155
+ headers: {
156
+ 'Content-Type': 'application/json'
157
+ },
158
+ body: JSON.stringify(thread.toDict())
159
+ });
160
+
161
+ if (!response.ok) {
162
+ throw new Error(`Push failed: ${response.status}`);
163
+ }
164
+
165
+ const data = await response.json();
166
+ showSyncIndicator('Saved', 'success');
167
+ return data;
168
+
169
+ } catch (error) {
170
+ console.error('Push error:', error);
171
+ showSyncIndicator('Save failed', 'error');
172
+ throw error;
173
+ }
174
+ }
175
+ review.pushThread = pushThread;
176
+
177
+ /**
178
+ * Push a comment to an existing thread
179
+ * @param {string} reqId - Requirement ID
180
+ * @param {string} threadId - Thread ID
181
+ * @param {Comment} comment - Comment to push
182
+ * @returns {Promise<Object>} Response data
183
+ */
184
+ async function pushComment(reqId, threadId, comment) {
185
+ if (!review.state.config.pushOnComment) {
186
+ return null;
187
+ }
188
+
189
+ showSyncIndicator('Saving...');
190
+
191
+ try {
192
+ const url = `${review.syncConfig.apiEndpoint}/reqs/${review.normalizeReqId(reqId)}/threads/${threadId}/comments`;
193
+ const response = await fetchWithRetry(url, {
194
+ method: 'POST',
195
+ headers: {
196
+ 'Content-Type': 'application/json'
197
+ },
198
+ body: JSON.stringify(comment.toDict())
199
+ });
200
+
201
+ if (!response.ok) {
202
+ throw new Error(`Push failed: ${response.status}`);
203
+ }
204
+
205
+ const data = await response.json();
206
+ showSyncIndicator('Saved', 'success');
207
+ return data;
208
+
209
+ } catch (error) {
210
+ console.error('Push error:', error);
211
+ showSyncIndicator('Save failed', 'error');
212
+ throw error;
213
+ }
214
+ }
215
+ review.pushComment = pushComment;
216
+
217
+ /**
218
+ * Push status request to server
219
+ * @param {StatusRequest} request - Request to push
220
+ * @returns {Promise<Object>} Response data
221
+ */
222
+ async function pushStatusRequest(request) {
223
+ showSyncIndicator('Saving...');
224
+
225
+ try {
226
+ const url = `${review.syncConfig.apiEndpoint}/reqs/${review.normalizeReqId(request.reqId)}/requests`;
227
+ const response = await fetchWithRetry(url, {
228
+ method: 'POST',
229
+ headers: {
230
+ 'Content-Type': 'application/json'
231
+ },
232
+ body: JSON.stringify(request.toDict())
233
+ });
234
+
235
+ if (!response.ok) {
236
+ throw new Error(`Push failed: ${response.status}`);
237
+ }
238
+
239
+ const data = await response.json();
240
+ showSyncIndicator('Saved', 'success');
241
+ return data;
242
+
243
+ } catch (error) {
244
+ console.error('Push error:', error);
245
+ showSyncIndicator('Save failed', 'error');
246
+ throw error;
247
+ }
248
+ }
249
+ review.pushStatusRequest = pushStatusRequest;
250
+
251
+ /**
252
+ * Push approval to server
253
+ * @param {string} reqId - Requirement ID
254
+ * @param {string} requestId - Request ID
255
+ * @param {Approval} approval - Approval to push
256
+ * @returns {Promise<Object>} Response data
257
+ */
258
+ async function pushApproval(reqId, requestId, approval) {
259
+ showSyncIndicator('Saving...');
260
+
261
+ try {
262
+ const url = `${review.syncConfig.apiEndpoint}/reqs/${review.normalizeReqId(reqId)}/requests/${requestId}/approvals`;
263
+ const response = await fetchWithRetry(url, {
264
+ method: 'POST',
265
+ headers: {
266
+ 'Content-Type': 'application/json'
267
+ },
268
+ body: JSON.stringify(approval.toDict())
269
+ });
270
+
271
+ if (!response.ok) {
272
+ throw new Error(`Push failed: ${response.status}`);
273
+ }
274
+
275
+ const data = await response.json();
276
+ showSyncIndicator('Saved', 'success');
277
+ return data;
278
+
279
+ } catch (error) {
280
+ console.error('Push error:', error);
281
+ showSyncIndicator('Save failed', 'error');
282
+ throw error;
283
+ }
284
+ }
285
+ review.pushApproval = pushApproval;
286
+
287
+ // ==========================================================================
288
+ // Helper Functions
289
+ // ==========================================================================
290
+
291
+ /**
292
+ * Fetch with retry logic
293
+ * @param {string} url - URL to fetch
294
+ * @param {Object} options - Fetch options
295
+ * @returns {Promise<Response>} Response
296
+ */
297
+ async function fetchWithRetry(url, options) {
298
+ let lastError;
299
+
300
+ for (let i = 0; i < review.syncConfig.retryAttempts; i++) {
301
+ try {
302
+ return await fetch(url, options);
303
+ } catch (error) {
304
+ lastError = error;
305
+ if (i < review.syncConfig.retryAttempts - 1) {
306
+ await new Promise(r => setTimeout(r, review.syncConfig.retryDelay));
307
+ }
308
+ }
309
+ }
310
+
311
+ throw lastError;
312
+ }
313
+
314
+ /**
315
+ * Show sync status indicator
316
+ * @param {string} message - Status message
317
+ * @param {string} type - Status type ('', 'success', 'error')
318
+ */
319
+ function showSyncIndicator(message, type = '') {
320
+ let indicator = document.querySelector('.rs-sync-indicator');
321
+
322
+ if (!indicator) {
323
+ indicator = document.createElement('div');
324
+ indicator.className = 'rs-sync-indicator';
325
+ document.body.appendChild(indicator);
326
+ }
327
+
328
+ indicator.textContent = message;
329
+ indicator.className = `rs-sync-indicator rs-sync-${type}`;
330
+ indicator.style.display = 'block';
331
+
332
+ // Auto-hide after success/error
333
+ if (type) {
334
+ setTimeout(() => {
335
+ indicator.style.display = 'none';
336
+ }, 3000);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Hide sync indicator
342
+ */
343
+ function hideSyncIndicator() {
344
+ const indicator = document.querySelector('.rs-sync-indicator');
345
+ if (indicator) {
346
+ indicator.style.display = 'none';
347
+ }
348
+ }
349
+
350
+ // ==========================================================================
351
+ // Auto-Sync
352
+ // ==========================================================================
353
+
354
+ /**
355
+ * Start auto-fetch timer
356
+ */
357
+ function startAutoFetch() {
358
+ if (autoFetchTimer) {
359
+ clearInterval(autoFetchTimer);
360
+ }
361
+
362
+ if (review.state.config.autoFetchOnOpen) {
363
+ autoFetchTimer = setInterval(() => {
364
+ fetchReviewData().catch(console.error);
365
+ }, review.syncConfig.autoFetchInterval);
366
+ }
367
+ }
368
+ review.startAutoFetch = startAutoFetch;
369
+
370
+ /**
371
+ * Stop auto-fetch timer
372
+ */
373
+ function stopAutoFetch() {
374
+ if (autoFetchTimer) {
375
+ clearInterval(autoFetchTimer);
376
+ autoFetchTimer = null;
377
+ }
378
+ }
379
+ review.stopAutoFetch = stopAutoFetch;
380
+
381
+ /**
382
+ * Get last sync time
383
+ * @returns {Date|null} Last sync timestamp
384
+ */
385
+ function getLastSyncTime() {
386
+ return lastSyncTime;
387
+ }
388
+ review.getLastSyncTime = getLastSyncTime;
389
+
390
+ /**
391
+ * Check if currently syncing
392
+ * @returns {boolean} True if sync in progress
393
+ */
394
+ function isSyncInProgress() {
395
+ return isSyncing;
396
+ }
397
+ review.isSyncInProgress = isSyncInProgress;
398
+
399
+ // ==========================================================================
400
+ // Conflict Handling
401
+ // ==========================================================================
402
+
403
+ /**
404
+ * Show conflict resolution dialog
405
+ * @param {Object} localData - Local version
406
+ * @param {Object} remoteData - Remote version
407
+ * @returns {Promise<string>} Resolution choice ('local', 'remote', 'merge')
408
+ */
409
+ async function showConflictDialog(localData, remoteData) {
410
+ return new Promise((resolve) => {
411
+ const overlay = document.createElement('div');
412
+ overlay.className = 'rs-conflict-overlay';
413
+ overlay.innerHTML = `
414
+ <div class="rs-conflict-dialog">
415
+ <h3>Sync Conflict Detected</h3>
416
+ <p>Your local changes conflict with remote changes.</p>
417
+ <div class="rs-conflict-options">
418
+ <button class="rs-btn rs-btn-local">Keep Local</button>
419
+ <button class="rs-btn rs-btn-remote">Use Remote</button>
420
+ <button class="rs-btn rs-btn-merge">Merge Both</button>
421
+ </div>
422
+ </div>
423
+ `;
424
+
425
+ overlay.querySelector('.rs-btn-local').addEventListener('click', () => {
426
+ document.body.removeChild(overlay);
427
+ resolve('local');
428
+ });
429
+
430
+ overlay.querySelector('.rs-btn-remote').addEventListener('click', () => {
431
+ document.body.removeChild(overlay);
432
+ resolve('remote');
433
+ });
434
+
435
+ overlay.querySelector('.rs-btn-merge').addEventListener('click', () => {
436
+ document.body.removeChild(overlay);
437
+ resolve('merge');
438
+ });
439
+
440
+ document.body.appendChild(overlay);
441
+ });
442
+ }
443
+ review.showConflictDialog = showConflictDialog;
444
+
445
+ // ==========================================================================
446
+ // UI Components
447
+ // ==========================================================================
448
+
449
+ /**
450
+ * Create refresh button HTML
451
+ * @returns {string} HTML
452
+ */
453
+ function createRefreshButton() {
454
+ return `
455
+ <button class="rs-btn rs-refresh-btn" title="Refresh review data">
456
+ Refresh
457
+ </button>
458
+ `;
459
+ }
460
+ review.createRefreshButton = createRefreshButton;
461
+
462
+ /**
463
+ * Create sync status display HTML
464
+ * @returns {string} HTML
465
+ */
466
+ function createSyncStatus() {
467
+ const time = lastSyncTime ? formatTime(lastSyncTime) : 'Never';
468
+ return `
469
+ <span class="rs-sync-status">
470
+ Last sync: ${time}
471
+ </span>
472
+ `;
473
+ }
474
+ review.createSyncStatus = createSyncStatus;
475
+
476
+ function formatTime(date) {
477
+ if (!date) return 'Never';
478
+ const now = new Date();
479
+ const diff = now - date;
480
+
481
+ if (diff < 60000) return 'just now';
482
+ if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
483
+ return date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
484
+ }
485
+
486
+ // ==========================================================================
487
+ // Event Listeners for Auto-Push
488
+ // ==========================================================================
489
+
490
+ // Listen for thread creation
491
+ document.addEventListener('traceview:thread-created', async (e) => {
492
+ const { thread } = e.detail;
493
+ try {
494
+ await pushThread(thread);
495
+ } catch (error) {
496
+ console.error('Failed to push thread:', error);
497
+ }
498
+ });
499
+
500
+ // Listen for comment additions
501
+ document.addEventListener('traceview:comment-added', async (e) => {
502
+ const { thread, reqId, body } = e.detail;
503
+ const comment = thread.comments[thread.comments.length - 1];
504
+ try {
505
+ await pushComment(reqId, thread.threadId, comment);
506
+ } catch (error) {
507
+ console.error('Failed to push comment:', error);
508
+ }
509
+ });
510
+
511
+ // Listen for status request creation
512
+ document.addEventListener('traceview:request-created', async (e) => {
513
+ const { request } = e.detail;
514
+ try {
515
+ await pushStatusRequest(request);
516
+ } catch (error) {
517
+ console.error('Failed to push request:', error);
518
+ }
519
+ });
520
+
521
+ // Listen for approval additions
522
+ document.addEventListener('traceview:approval-added', async (e) => {
523
+ const { request, reqId, user, decision } = e.detail;
524
+ const approval = request.approvals[request.approvals.length - 1];
525
+ try {
526
+ await pushApproval(reqId, request.requestId, approval);
527
+ } catch (error) {
528
+ console.error('Failed to push approval:', error);
529
+ }
530
+ });
531
+
532
+ // REQ-d00099: Listen for archive events to refresh sync status
533
+ document.addEventListener('traceview:archive-view-opened', async (e) => {
534
+ console.log('Archive view opened:', e.detail.package?.name);
535
+ // Update git sync indicator to show read-only mode
536
+ updateGitSyncIndicator();
537
+ });
538
+
539
+ document.addEventListener('traceview:archive-view-closed', async () => {
540
+ console.log('Archive view closed');
541
+ // Restore normal git sync indicator
542
+ updateGitSyncIndicator();
543
+ });
544
+
545
+ // ==========================================================================
546
+ // Initialization
547
+ // ==========================================================================
548
+
549
+ /**
550
+ * Initialize sync module
551
+ * @param {Object} embeddedData - Embedded review data from page
552
+ */
553
+ function initSync(embeddedData) {
554
+ // Load embedded data
555
+ if (embeddedData) {
556
+ review.state.loadFromEmbedded(embeddedData);
557
+ lastSyncTime = new Date();
558
+ }
559
+
560
+ // Start auto-fetch if enabled
561
+ if (review.state.config.autoFetchOnOpen) {
562
+ startAutoFetch();
563
+ }
564
+
565
+ // Initialize git sync status
566
+ initGitSync();
567
+
568
+ console.log('Review sync initialized');
569
+ }
570
+ review.initSync = initSync;
571
+
572
+ // ==========================================================================
573
+ // Git Sync Operations
574
+ // ==========================================================================
575
+
576
+ // Git sync state
577
+ let gitSyncStatus = null;
578
+ let isGitSyncing = false;
579
+ let autoSyncEnabled = true;
580
+
581
+ /**
582
+ * Fetch git sync status from server
583
+ */
584
+ async function fetchGitSyncStatus() {
585
+ try {
586
+ const response = await fetch('/api/reviews/sync/status');
587
+ if (!response.ok) {
588
+ throw new Error(`HTTP ${response.status}`);
589
+ }
590
+ gitSyncStatus = await response.json();
591
+ autoSyncEnabled = gitSyncStatus.auto_sync_enabled !== false;
592
+ updateGitSyncIndicator();
593
+ return gitSyncStatus;
594
+ } catch (error) {
595
+ console.error('Failed to fetch git sync status:', error);
596
+ return null;
597
+ }
598
+ }
599
+ review.fetchGitSyncStatus = fetchGitSyncStatus;
600
+
601
+ /**
602
+ * Manually trigger a git sync (commit + push)
603
+ */
604
+ async function gitPush(message) {
605
+ if (isGitSyncing) {
606
+ return { success: false, error: 'Sync already in progress' };
607
+ }
608
+
609
+ isGitSyncing = true;
610
+ updateGitSyncIndicator();
611
+
612
+ try {
613
+ const response = await fetch('/api/reviews/sync/push', {
614
+ method: 'POST',
615
+ headers: { 'Content-Type': 'application/json' },
616
+ body: JSON.stringify({
617
+ user: review.state.currentUser || 'anonymous',
618
+ message: message || 'Manual sync'
619
+ })
620
+ });
621
+
622
+ if (!response.ok) {
623
+ throw new Error(`HTTP ${response.status}`);
624
+ }
625
+
626
+ const result = await response.json();
627
+
628
+ if (result.success) {
629
+ showSyncIndicator('Git push completed', 'success');
630
+ } else if (result.error) {
631
+ showSyncIndicator(`Git sync error: ${result.error}`, 'error');
632
+ }
633
+
634
+ // Refresh status
635
+ await fetchGitSyncStatus();
636
+
637
+ return result;
638
+ } catch (error) {
639
+ console.error('Git sync failed:', error);
640
+ showSyncIndicator(`Git sync failed: ${error.message}`, 'error');
641
+ return { success: false, error: error.message };
642
+ } finally {
643
+ isGitSyncing = false;
644
+ updateGitSyncIndicator();
645
+ }
646
+ }
647
+ review.gitPush = gitPush;
648
+
649
+ /**
650
+ * Fetch latest review data from remote git
651
+ */
652
+ async function gitFetch() {
653
+ if (isGitSyncing) {
654
+ return { success: false, error: 'Sync already in progress' };
655
+ }
656
+
657
+ isGitSyncing = true;
658
+ updateGitSyncIndicator();
659
+
660
+ try {
661
+ const response = await fetch('/api/reviews/sync/fetch', {
662
+ method: 'POST'
663
+ });
664
+
665
+ if (!response.ok) {
666
+ throw new Error(`HTTP ${response.status}`);
667
+ }
668
+
669
+ const result = await response.json();
670
+
671
+ if (result.success && result.merged) {
672
+ showSyncIndicator('Updated from git remote', 'success');
673
+ // Reload review data if merged
674
+ await fetchReviewData();
675
+ } else if (result.error) {
676
+ showSyncIndicator(`Git fetch error: ${result.error}`, 'error');
677
+ }
678
+
679
+ // Refresh status
680
+ await fetchGitSyncStatus();
681
+
682
+ return result;
683
+ } catch (error) {
684
+ console.error('Git fetch failed:', error);
685
+ showSyncIndicator(`Git fetch failed: ${error.message}`, 'error');
686
+ return { success: false, error: error.message };
687
+ } finally {
688
+ isGitSyncing = false;
689
+ updateGitSyncIndicator();
690
+ }
691
+ }
692
+ review.gitFetch = gitFetch;
693
+
694
+ /**
695
+ * Update the git sync status indicator
696
+ * Shows package context when on a review branch
697
+ */
698
+ function updateGitSyncIndicator() {
699
+ const indicator = document.getElementById('gitSyncIndicator');
700
+ if (!indicator) return;
701
+
702
+ if (isGitSyncing) {
703
+ indicator.innerHTML = '<span class="git-sync-icon syncing">&#x21bb;</span> Git syncing...';
704
+ indicator.className = 'git-sync-indicator syncing';
705
+ return;
706
+ }
707
+
708
+ if (!gitSyncStatus) {
709
+ indicator.innerHTML = '<span class="git-sync-icon">&#x2300;</span> Git offline';
710
+ indicator.className = 'git-sync-indicator offline';
711
+ return;
712
+ }
713
+
714
+ let html = '';
715
+ let className = 'git-sync-indicator';
716
+
717
+ // Show package branch context if available
718
+ const currentBranch = review.packages && review.packages.currentBranch;
719
+ if (currentBranch && currentBranch.startsWith('reviews/')) {
720
+ // Parse branch: reviews/{package}/{user}
721
+ const parts = currentBranch.replace('reviews/', '').split('/');
722
+ if (parts.length >= 2) {
723
+ const packageName = parts[0];
724
+ const userName = parts[1];
725
+ html += `<span class="git-branch-context" title="${currentBranch}">${packageName}/${userName}</span> `;
726
+ }
727
+ }
728
+
729
+ if (gitSyncStatus.has_local_changes) {
730
+ html += '<span class="git-sync-icon pending">&#x2022;</span> ';
731
+ className += ' pending';
732
+ } else {
733
+ html += '<span class="git-sync-icon synced">&#x2713;</span> ';
734
+ }
735
+
736
+ if (gitSyncStatus.ahead > 0) {
737
+ html += `<span class="git-ahead">&uarr;${gitSyncStatus.ahead}</span> `;
738
+ }
739
+ if (gitSyncStatus.behind > 0) {
740
+ html += `<span class="git-behind">&darr;${gitSyncStatus.behind}</span> `;
741
+ className += ' behind';
742
+ }
743
+
744
+ if (gitSyncStatus.has_local_changes) {
745
+ html += 'Changes pending';
746
+ } else if (gitSyncStatus.ahead > 0 || gitSyncStatus.behind > 0) {
747
+ html += 'Git sync needed';
748
+ } else {
749
+ html += 'Git synced';
750
+ className += ' synced';
751
+ }
752
+
753
+ indicator.innerHTML = html;
754
+ indicator.className = className;
755
+ }
756
+ review.updateGitSyncIndicator = updateGitSyncIndicator;
757
+
758
+ /**
759
+ * Handle sync result from API response
760
+ */
761
+ function handleGitSyncResult(result) {
762
+ if (!result || !result.sync) return;
763
+
764
+ const sync = result.sync;
765
+ if (sync.success) {
766
+ if (sync.committed) {
767
+ console.log('Review data committed to git');
768
+ }
769
+ if (sync.pushed) {
770
+ console.log('Review data pushed to git remote');
771
+ }
772
+ } else if (sync.error) {
773
+ console.warn('Git sync issue:', sync.error);
774
+ }
775
+
776
+ // Refresh sync status
777
+ fetchGitSyncStatus();
778
+ }
779
+ review.handleGitSyncResult = handleGitSyncResult;
780
+
781
+ /**
782
+ * Create git sync controls HTML
783
+ */
784
+ function createGitSyncControls() {
785
+ return `
786
+ <div class="git-sync-controls">
787
+ <span id="gitSyncIndicator" class="git-sync-indicator">
788
+ <span class="git-sync-icon">&#x21bb;</span> Checking...
789
+ </span>
790
+ <div class="git-sync-buttons">
791
+ <button class="rs-btn rs-btn-sm" onclick="TraceView.review.gitFetch()" title="Fetch from git remote">
792
+ &#x2193; Fetch
793
+ </button>
794
+ <button class="rs-btn rs-btn-sm" onclick="TraceView.review.gitPush()" title="Push to git remote">
795
+ &#x2191; Push
796
+ </button>
797
+ <button class="rs-btn rs-btn-sm rs-btn-fetch-all" onclick="TraceView.review.fetchAllPackageUsers()" title="Fetch data from all package contributors">
798
+ &#x21c4; Fetch All
799
+ </button>
800
+ </div>
801
+ </div>
802
+ `;
803
+ }
804
+ review.createGitSyncControls = createGitSyncControls;
805
+
806
+ /**
807
+ * Fetch consolidated review data from all users' branches for the current package.
808
+ * This merges data from reviews/{package}/alice, reviews/{package}/bob, etc.
809
+ */
810
+ async function fetchAllPackageUsers() {
811
+ if (isGitSyncing) {
812
+ return { success: false, error: 'Sync already in progress' };
813
+ }
814
+
815
+ isGitSyncing = true;
816
+ updateGitSyncIndicator();
817
+
818
+ try {
819
+ const response = await fetch('/api/reviews/sync/fetch-all-package', {
820
+ method: 'POST'
821
+ });
822
+
823
+ if (!response.ok) {
824
+ throw new Error(`HTTP ${response.status}`);
825
+ }
826
+
827
+ const result = await response.json();
828
+
829
+ if (result.contributors && result.contributors.length > 0) {
830
+ showSyncIndicator(`Loaded data from ${result.contributors.length} contributor(s)`, 'success');
831
+
832
+ // Store contributors list
833
+ if (review.packages) {
834
+ review.packages.contributors = result.contributors;
835
+ }
836
+
837
+ // Trigger refresh event so UI updates with merged data
838
+ document.dispatchEvent(new CustomEvent('traceview:data-fetched', {
839
+ detail: { data: result, timestamp: new Date() }
840
+ }));
841
+ } else if (result.error) {
842
+ showSyncIndicator(`Fetch error: ${result.error}`, 'error');
843
+ } else {
844
+ showSyncIndicator('No contributors found', 'success');
845
+ }
846
+
847
+ // Refresh sync status
848
+ await fetchGitSyncStatus();
849
+
850
+ return result;
851
+ } catch (error) {
852
+ console.error('Fetch all package users failed:', error);
853
+ showSyncIndicator(`Fetch failed: ${error.message}`, 'error');
854
+ return { success: false, error: error.message };
855
+ } finally {
856
+ isGitSyncing = false;
857
+ updateGitSyncIndicator();
858
+ }
859
+ }
860
+ review.fetchAllPackageUsers = fetchAllPackageUsers;
861
+
862
+ /**
863
+ * Initialize git sync on page load
864
+ */
865
+ async function initGitSync() {
866
+ // Fetch initial sync status
867
+ await fetchGitSyncStatus();
868
+
869
+ // Auto-fetch on load if behind
870
+ if (gitSyncStatus && gitSyncStatus.behind > 0) {
871
+ console.log('Behind git remote, fetching updates...');
872
+ await gitFetch();
873
+ }
874
+
875
+ // Periodically check sync status (every 30 seconds)
876
+ setInterval(fetchGitSyncStatus, 30000);
877
+ }
878
+ review.initGitSync = initGitSync;
879
+
880
+ // Inject git sync styles
881
+ const gitSyncStyles = `
882
+ .git-sync-indicator {
883
+ display: inline-flex;
884
+ align-items: center;
885
+ gap: 4px;
886
+ padding: 4px 8px;
887
+ border-radius: 4px;
888
+ font-size: 12px;
889
+ background: #f5f5f5;
890
+ color: #666;
891
+ }
892
+
893
+ .git-sync-indicator.synced {
894
+ background: #e8f5e9;
895
+ color: #2e7d32;
896
+ }
897
+
898
+ .git-sync-indicator.pending {
899
+ background: #fff3e0;
900
+ color: #ef6c00;
901
+ }
902
+
903
+ .git-sync-indicator.behind {
904
+ background: #e3f2fd;
905
+ color: #1565c0;
906
+ }
907
+
908
+ .git-sync-indicator.syncing {
909
+ background: #f3e5f5;
910
+ color: #7b1fa2;
911
+ }
912
+
913
+ .git-sync-indicator.offline {
914
+ background: #fafafa;
915
+ color: #999;
916
+ }
917
+
918
+ .git-sync-icon {
919
+ font-size: 14px;
920
+ }
921
+
922
+ .git-sync-icon.syncing {
923
+ animation: git-spin 1s linear infinite;
924
+ }
925
+
926
+ .git-sync-icon.pending {
927
+ color: #ff9800;
928
+ }
929
+
930
+ .git-sync-icon.synced {
931
+ color: #4caf50;
932
+ }
933
+
934
+ @keyframes git-spin {
935
+ from { transform: rotate(0deg); }
936
+ to { transform: rotate(360deg); }
937
+ }
938
+
939
+ .git-ahead {
940
+ color: #4caf50;
941
+ font-weight: bold;
942
+ }
943
+
944
+ .git-behind {
945
+ color: #2196f3;
946
+ font-weight: bold;
947
+ }
948
+
949
+ .git-sync-controls {
950
+ display: flex;
951
+ align-items: center;
952
+ gap: 12px;
953
+ padding: 8px;
954
+ background: #fafafa;
955
+ border-radius: 4px;
956
+ border: 1px solid #e0e0e0;
957
+ margin-bottom: 8px;
958
+ }
959
+
960
+ .git-sync-buttons {
961
+ display: flex;
962
+ gap: 4px;
963
+ }
964
+
965
+ .git-branch-context {
966
+ display: inline-block;
967
+ padding: 2px 6px;
968
+ background: #e3f2fd;
969
+ color: #1565c0;
970
+ border-radius: 3px;
971
+ font-size: 11px;
972
+ font-weight: 500;
973
+ margin-right: 4px;
974
+ cursor: default;
975
+ }
976
+
977
+ .git-branch-context:hover {
978
+ background: #bbdefb;
979
+ }
980
+ `;
981
+
982
+ // Inject git sync styles on load
983
+ (function injectGitSyncStyles() {
984
+ if (document.getElementById('git-sync-styles')) return;
985
+
986
+ const style = document.createElement('style');
987
+ style.id = 'git-sync-styles';
988
+ style.textContent = gitSyncStyles;
989
+ document.head.appendChild(style);
990
+ })();
991
+
992
+ })(TraceView.review);