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.
- elspais/cli.py +141 -10
- elspais/commands/hash_cmd.py +72 -26
- elspais/commands/reformat_cmd.py +458 -0
- elspais/commands/trace.py +157 -3
- elspais/commands/validate.py +44 -16
- elspais/core/models.py +2 -0
- elspais/core/parser.py +68 -24
- elspais/reformat/__init__.py +50 -0
- elspais/reformat/detector.py +119 -0
- elspais/reformat/hierarchy.py +246 -0
- elspais/reformat/line_breaks.py +220 -0
- elspais/reformat/prompts.py +123 -0
- elspais/reformat/transformer.py +264 -0
- elspais/sponsors/__init__.py +432 -0
- elspais/trace_view/__init__.py +54 -0
- elspais/trace_view/coverage.py +183 -0
- elspais/trace_view/generators/__init__.py +12 -0
- elspais/trace_view/generators/base.py +329 -0
- elspais/trace_view/generators/csv.py +122 -0
- elspais/trace_view/generators/markdown.py +175 -0
- elspais/trace_view/html/__init__.py +31 -0
- elspais/trace_view/html/generator.py +1006 -0
- elspais/trace_view/html/templates/base.html +283 -0
- elspais/trace_view/html/templates/components/code_viewer_modal.html +14 -0
- elspais/trace_view/html/templates/components/file_picker_modal.html +20 -0
- elspais/trace_view/html/templates/components/legend_modal.html +69 -0
- elspais/trace_view/html/templates/components/review_panel.html +118 -0
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +244 -0
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +77 -0
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +237 -0
- elspais/trace_view/html/templates/partials/review/review-comments.js +928 -0
- elspais/trace_view/html/templates/partials/review/review-data.js +961 -0
- elspais/trace_view/html/templates/partials/review/review-help.js +679 -0
- elspais/trace_view/html/templates/partials/review/review-init.js +177 -0
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +429 -0
- elspais/trace_view/html/templates/partials/review/review-packages.js +1029 -0
- elspais/trace_view/html/templates/partials/review/review-position.js +540 -0
- elspais/trace_view/html/templates/partials/review/review-resize.js +115 -0
- elspais/trace_view/html/templates/partials/review/review-status.js +659 -0
- elspais/trace_view/html/templates/partials/review/review-sync.js +992 -0
- elspais/trace_view/html/templates/partials/review-styles.css +2238 -0
- elspais/trace_view/html/templates/partials/scripts.js +1741 -0
- elspais/trace_view/html/templates/partials/styles.css +1756 -0
- elspais/trace_view/models.py +353 -0
- elspais/trace_view/review/__init__.py +60 -0
- elspais/trace_view/review/branches.py +1149 -0
- elspais/trace_view/review/models.py +1205 -0
- elspais/trace_view/review/position.py +609 -0
- elspais/trace_view/review/server.py +1056 -0
- elspais/trace_view/review/status.py +470 -0
- elspais/trace_view/review/storage.py +1367 -0
- elspais/trace_view/scanning.py +213 -0
- elspais/trace_view/specs/README.md +84 -0
- elspais/trace_view/specs/tv-d00001-template-architecture.md +36 -0
- elspais/trace_view/specs/tv-d00002-css-extraction.md +37 -0
- elspais/trace_view/specs/tv-d00003-js-extraction.md +43 -0
- elspais/trace_view/specs/tv-d00004-build-embedding.md +40 -0
- elspais/trace_view/specs/tv-d00005-test-format.md +78 -0
- elspais/trace_view/specs/tv-d00010-review-data-models.md +33 -0
- elspais/trace_view/specs/tv-d00011-review-storage.md +33 -0
- elspais/trace_view/specs/tv-d00012-position-resolution.md +33 -0
- elspais/trace_view/specs/tv-d00013-git-branches.md +31 -0
- elspais/trace_view/specs/tv-d00014-review-api-server.md +31 -0
- elspais/trace_view/specs/tv-d00015-status-modifier.md +27 -0
- elspais/trace_view/specs/tv-d00016-js-integration.md +33 -0
- elspais/trace_view/specs/tv-p00001-html-generator.md +33 -0
- elspais/trace_view/specs/tv-p00002-review-system.md +29 -0
- {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/METADATA +36 -18
- elspais-0.11.1.dist-info/RECORD +101 -0
- elspais-0.9.3.dist-info/RECORD +0 -40
- {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/WHEEL +0 -0
- {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/entry_points.txt +0 -0
- {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">↻</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">⌀</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">•</span> ';
|
|
731
|
+
className += ' pending';
|
|
732
|
+
} else {
|
|
733
|
+
html += '<span class="git-sync-icon synced">✓</span> ';
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (gitSyncStatus.ahead > 0) {
|
|
737
|
+
html += `<span class="git-ahead">↑${gitSyncStatus.ahead}</span> `;
|
|
738
|
+
}
|
|
739
|
+
if (gitSyncStatus.behind > 0) {
|
|
740
|
+
html += `<span class="git-behind">↓${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">↻</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
|
+
↓ Fetch
|
|
793
|
+
</button>
|
|
794
|
+
<button class="rs-btn rs-btn-sm" onclick="TraceView.review.gitPush()" title="Push to git remote">
|
|
795
|
+
↑ 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
|
+
⇄ 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);
|