git-watchtower 1.10.3 → 1.10.5
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/package.json +2 -2
- package/src/server/web-ui/js.js +323 -407
- package/src/server/web-ui/pure.js +137 -0
- package/src/server/web.js +1 -1
package/src/server/web-ui/js.js
CHANGED
|
@@ -2,69 +2,87 @@
|
|
|
2
2
|
* Client-side JavaScript for the Git Watchtower web dashboard.
|
|
3
3
|
* Contains all interactive behavior: SSE connection, rendering, keyboard
|
|
4
4
|
* navigation, modals, notifications, and preferences.
|
|
5
|
+
*
|
|
6
|
+
* Pure utility functions (escHtml, timeAgo, etc.) live in pure.js and are
|
|
7
|
+
* inlined here at assembly time so they can be unit-tested in Node.
|
|
5
8
|
* @module server/web-ui/js
|
|
6
9
|
*/
|
|
7
10
|
|
|
11
|
+
const pureFns = require('./pure');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Serialize pure functions into a block of JS source that can be embedded
|
|
15
|
+
* in a browser <script>. Each function is emitted verbatim using
|
|
16
|
+
* Function.prototype.toString().
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function inlinePureFunctions() {
|
|
20
|
+
return Object.entries(pureFns)
|
|
21
|
+
.map(([name, fn]) => ` var ${name} = ${fn.toString()};`)
|
|
22
|
+
.join('\n\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
/**
|
|
9
26
|
* Get the dashboard client-side JavaScript.
|
|
10
27
|
* @returns {string} JavaScript content (without script tags)
|
|
11
28
|
*/
|
|
12
29
|
function getDashboardJs() {
|
|
30
|
+
const pureFnBlock = inlinePureFunctions();
|
|
13
31
|
return `
|
|
14
32
|
(function() {
|
|
15
33
|
'use strict';
|
|
16
34
|
|
|
17
35
|
// ── State ──────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
let state = null;
|
|
37
|
+
let prevBranches = null; // for notification diffing
|
|
38
|
+
let selectedIndex = 0;
|
|
39
|
+
let searchMode = false;
|
|
40
|
+
let searchQuery = '';
|
|
41
|
+
let confirmMode = false;
|
|
42
|
+
let confirmCallback = null;
|
|
43
|
+
let connected = false;
|
|
44
|
+
let flashTimer = null;
|
|
45
|
+
let activeTabId = null;
|
|
46
|
+
let logViewerMode = false;
|
|
47
|
+
let logViewerTab = 'server';
|
|
48
|
+
let branchActionMode = false;
|
|
49
|
+
let infoMode = false;
|
|
50
|
+
let cleanupMode = false;
|
|
51
|
+
let updateMode = false;
|
|
52
|
+
let stashMode = false;
|
|
53
|
+
let pendingStashBranch = null;
|
|
54
|
+
let updateNotificationShown = false;
|
|
55
|
+
let remoteTabPollTimer = null;
|
|
38
56
|
|
|
39
57
|
// ── Persistent Preferences (localStorage) ─────────────────────
|
|
40
|
-
|
|
58
|
+
const PREFS_KEY = 'git-watchtower-prefs';
|
|
41
59
|
function loadPrefs() {
|
|
42
60
|
try {
|
|
43
61
|
return JSON.parse(localStorage.getItem(PREFS_KEY)) || {};
|
|
44
62
|
} catch (e) { return {}; }
|
|
45
63
|
}
|
|
46
64
|
function savePrefs(updates) {
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
const prefs = loadPrefs();
|
|
66
|
+
Object.keys(updates).forEach((k) => { prefs[k] = updates[k]; });
|
|
49
67
|
try { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); } catch (e) { /* ignore */ }
|
|
50
68
|
return prefs;
|
|
51
69
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
const prefs = loadPrefs();
|
|
71
|
+
let sidebarCollapsed = prefs.sidebarCollapsed || false;
|
|
72
|
+
let sortOrder = prefs.sortOrder || 'default';
|
|
73
|
+
let pinnedBranches = prefs.pinnedBranches || [];
|
|
56
74
|
|
|
57
75
|
// Apply initial sidebar state
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
{
|
|
77
|
+
const layout = document.querySelector('.layout');
|
|
60
78
|
if (sidebarCollapsed) layout.classList.add('sidebar-collapsed');
|
|
61
|
-
}
|
|
79
|
+
}
|
|
62
80
|
|
|
63
81
|
// ── Browser Notifications ─────────────────────────────────────
|
|
64
|
-
|
|
82
|
+
let notifPermission = typeof Notification !== 'undefined' ? Notification.permission : 'denied';
|
|
65
83
|
|
|
66
84
|
function updateNotifButton() {
|
|
67
|
-
|
|
85
|
+
const btn = document.getElementById('notif-btn');
|
|
68
86
|
if (notifPermission === 'granted') {
|
|
69
87
|
btn.className = 'notif-btn granted';
|
|
70
88
|
btn.textContent = 'notifs on';
|
|
@@ -78,13 +96,13 @@ function getDashboardJs() {
|
|
|
78
96
|
}
|
|
79
97
|
updateNotifButton();
|
|
80
98
|
|
|
81
|
-
document.getElementById('notif-btn').addEventListener('click',
|
|
99
|
+
document.getElementById('notif-btn').addEventListener('click', () => {
|
|
82
100
|
if (notifPermission === 'granted' || notifPermission === 'denied') return;
|
|
83
101
|
if (typeof Notification === 'undefined') {
|
|
84
102
|
showToast('Notifications not supported in this browser', 'warning');
|
|
85
103
|
return;
|
|
86
104
|
}
|
|
87
|
-
Notification.requestPermission().then(
|
|
105
|
+
Notification.requestPermission().then((perm) => {
|
|
88
106
|
notifPermission = perm;
|
|
89
107
|
updateNotifButton();
|
|
90
108
|
if (perm === 'granted') {
|
|
@@ -96,20 +114,19 @@ function getDashboardJs() {
|
|
|
96
114
|
function sendNotification(title, body, tag) {
|
|
97
115
|
if (notifPermission !== 'granted') return;
|
|
98
116
|
try {
|
|
99
|
-
|
|
100
|
-
setTimeout(
|
|
117
|
+
const n = new Notification(title, { body, tag: tag || 'git-watchtower', icon: '', silent: false });
|
|
118
|
+
setTimeout(() => n.close(), 8000);
|
|
101
119
|
} catch (e) { /* ignore */ }
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
function diffBranchesForNotifications(oldBranches, newBranches) {
|
|
105
123
|
if (!oldBranches || !newBranches) return;
|
|
106
|
-
|
|
107
|
-
for (
|
|
108
|
-
oldMap[
|
|
124
|
+
const oldMap = {};
|
|
125
|
+
for (const ob of oldBranches) {
|
|
126
|
+
oldMap[ob.name] = ob;
|
|
109
127
|
}
|
|
110
|
-
for (
|
|
111
|
-
|
|
112
|
-
var ob = oldMap[nb.name];
|
|
128
|
+
for (const nb of newBranches) {
|
|
129
|
+
const ob = oldMap[nb.name];
|
|
113
130
|
if (!ob && nb.isNew) {
|
|
114
131
|
sendNotification('New Branch', nb.name + ' was created', 'new-' + nb.name);
|
|
115
132
|
} else if (ob && !ob.justUpdated && nb.justUpdated) {
|
|
@@ -118,15 +135,9 @@ function getDashboardJs() {
|
|
|
118
135
|
}
|
|
119
136
|
// Check PR state changes
|
|
120
137
|
if (state && state.branchPrStatusMap) {
|
|
121
|
-
for (
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
if (pr && pr.state === 'MERGED') {
|
|
125
|
-
// Only notify once - check if it was not merged before
|
|
126
|
-
var oldBranch = oldMap[bn];
|
|
127
|
-
if (oldBranch) {
|
|
128
|
-
sendNotification('PR Merged', 'PR #' + pr.number + ' for ' + bn + ' was merged', 'merged-' + bn);
|
|
129
|
-
}
|
|
138
|
+
for (const [bn, pr] of Object.entries(state.branchPrStatusMap)) {
|
|
139
|
+
if (pr && pr.state === 'MERGED' && oldMap[bn]) {
|
|
140
|
+
sendNotification('PR Merged', 'PR #' + pr.number + ' for ' + bn + ' was merged', 'merged-' + bn);
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
}
|
|
@@ -134,17 +145,17 @@ function getDashboardJs() {
|
|
|
134
145
|
|
|
135
146
|
// ── Clipboard Helper ──────────────────────────────────────────
|
|
136
147
|
function copyToClipboard(text, btnEl) {
|
|
137
|
-
navigator.clipboard.writeText(text).then(
|
|
148
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
138
149
|
if (btnEl) {
|
|
139
150
|
btnEl.classList.add('copied');
|
|
140
151
|
btnEl.innerHTML = '✓';
|
|
141
|
-
setTimeout(
|
|
152
|
+
setTimeout(() => {
|
|
142
153
|
btnEl.classList.remove('copied');
|
|
143
154
|
btnEl.innerHTML = '📋';
|
|
144
155
|
}, 1500);
|
|
145
156
|
}
|
|
146
157
|
showToast('Copied: ' + text, 'success');
|
|
147
|
-
}).catch(
|
|
158
|
+
}).catch(() => {
|
|
148
159
|
showToast('Failed to copy', 'error');
|
|
149
160
|
});
|
|
150
161
|
}
|
|
@@ -154,19 +165,18 @@ function getDashboardJs() {
|
|
|
154
165
|
return (state && state.repoWebUrl) ? state.repoWebUrl.replace(/\\/tree\\/.*$/, '') : null;
|
|
155
166
|
}
|
|
156
167
|
function getBranchUrl(branchName) {
|
|
157
|
-
|
|
168
|
+
const base = getRepoUrl();
|
|
158
169
|
if (!base) return null;
|
|
159
170
|
return base + '/tree/' + encodeURIComponent(branchName);
|
|
160
171
|
}
|
|
161
172
|
function getCommitUrl(hash) {
|
|
162
|
-
|
|
173
|
+
const base = getRepoUrl();
|
|
163
174
|
if (!base || !hash) return null;
|
|
164
175
|
return base + '/commit/' + hash;
|
|
165
176
|
}
|
|
166
177
|
function getPrUrl(prNumber) {
|
|
167
|
-
|
|
178
|
+
const base = getRepoUrl();
|
|
168
179
|
if (!base || !prNumber) return null;
|
|
169
|
-
// Detect GitLab by URL pattern
|
|
170
180
|
if (base.indexOf('gitlab') !== -1) {
|
|
171
181
|
return base + '/-/merge_requests/' + prNumber;
|
|
172
182
|
}
|
|
@@ -174,20 +184,20 @@ function getDashboardJs() {
|
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
// ── SSE Connection ─────────────────────────────────────────────
|
|
177
|
-
|
|
187
|
+
let evtSource = null;
|
|
178
188
|
|
|
179
189
|
function connect() {
|
|
180
190
|
if (evtSource) { evtSource.close(); }
|
|
181
191
|
evtSource = new EventSource('/api/events');
|
|
182
192
|
|
|
183
|
-
evtSource.onopen =
|
|
193
|
+
evtSource.onopen = () => {
|
|
184
194
|
connected = true;
|
|
185
195
|
updateConnectionStatus();
|
|
186
196
|
};
|
|
187
197
|
|
|
188
|
-
evtSource.addEventListener('state',
|
|
198
|
+
evtSource.addEventListener('state', (e) => {
|
|
189
199
|
try {
|
|
190
|
-
|
|
200
|
+
const newState = JSON.parse(e.data);
|
|
191
201
|
if (!activeTabId && newState.activeProjectId) {
|
|
192
202
|
activeTabId = newState.activeProjectId;
|
|
193
203
|
}
|
|
@@ -196,16 +206,14 @@ function getDashboardJs() {
|
|
|
196
206
|
// data (branches, PRs, activity, etc.) — only update global
|
|
197
207
|
// metadata so the tab bar, connection status, and version info
|
|
198
208
|
// stay current.
|
|
199
|
-
|
|
209
|
+
const viewingLocalProject = !activeTabId || activeTabId === newState.activeProjectId;
|
|
200
210
|
if (viewingLocalProject) {
|
|
201
|
-
// Diff branches for desktop notifications
|
|
202
211
|
if (state && state.branches) {
|
|
203
212
|
diffBranchesForNotifications(state.branches, newState.branches || []);
|
|
204
213
|
}
|
|
205
214
|
prevBranches = state ? state.branches : null;
|
|
206
215
|
state = newState;
|
|
207
216
|
} else {
|
|
208
|
-
// Viewing a remote tab — preserve per-project fields, update globals only
|
|
209
217
|
if (state) {
|
|
210
218
|
state.projects = newState.projects;
|
|
211
219
|
state.version = newState.version;
|
|
@@ -221,16 +229,16 @@ function getDashboardJs() {
|
|
|
221
229
|
} catch (err) { /* ignore parse errors */ }
|
|
222
230
|
});
|
|
223
231
|
|
|
224
|
-
evtSource.addEventListener('flash',
|
|
232
|
+
evtSource.addEventListener('flash', (e) => {
|
|
225
233
|
try {
|
|
226
|
-
|
|
234
|
+
const data = JSON.parse(e.data);
|
|
227
235
|
showFlash(data.text, data.type);
|
|
228
236
|
} catch (err) { /* ignore */ }
|
|
229
237
|
});
|
|
230
238
|
|
|
231
|
-
evtSource.addEventListener('actionResult',
|
|
239
|
+
evtSource.addEventListener('actionResult', (e) => {
|
|
232
240
|
try {
|
|
233
|
-
|
|
241
|
+
const data = JSON.parse(e.data);
|
|
234
242
|
if (!data.success && data.message && data.message.indexOf('uncommitted') !== -1) {
|
|
235
243
|
pendingStashBranch = data.branch || null;
|
|
236
244
|
showErrorToastWithHint(data.message, 'Press S to stash');
|
|
@@ -240,15 +248,15 @@ function getDashboardJs() {
|
|
|
240
248
|
} catch (err) { /* ignore */ }
|
|
241
249
|
});
|
|
242
250
|
|
|
243
|
-
evtSource.onerror =
|
|
251
|
+
evtSource.onerror = () => {
|
|
244
252
|
connected = false;
|
|
245
253
|
updateConnectionStatus();
|
|
246
254
|
};
|
|
247
255
|
}
|
|
248
256
|
|
|
249
257
|
function updateConnectionStatus() {
|
|
250
|
-
|
|
251
|
-
|
|
258
|
+
const dot = document.getElementById('connection-dot');
|
|
259
|
+
const badge = document.getElementById('status-badge');
|
|
252
260
|
if (connected) {
|
|
253
261
|
dot.className = 'connection-dot connected';
|
|
254
262
|
badge.className = 'badge badge-online';
|
|
@@ -262,48 +270,98 @@ function getDashboardJs() {
|
|
|
262
270
|
|
|
263
271
|
// ── Actions ────────────────────────────────────────────────────
|
|
264
272
|
function sendAction(action, payload) {
|
|
265
|
-
|
|
273
|
+
const xhr = new XMLHttpRequest();
|
|
266
274
|
xhr.open('POST', '/api/action');
|
|
267
275
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
268
|
-
|
|
276
|
+
const data = { action, payload: payload || {} };
|
|
269
277
|
if (activeTabId) data.projectId = activeTabId;
|
|
270
278
|
xhr.send(JSON.stringify(data));
|
|
271
279
|
}
|
|
272
280
|
|
|
273
281
|
// ── Flash Messages ─────────────────────────────────────────────
|
|
274
282
|
function showFlash(text, type) {
|
|
275
|
-
|
|
283
|
+
const el = document.getElementById('flash');
|
|
276
284
|
el.textContent = text;
|
|
277
285
|
el.className = 'flash visible ' + (type || 'info');
|
|
278
286
|
clearTimeout(flashTimer);
|
|
279
|
-
flashTimer = setTimeout(
|
|
280
|
-
el.className = 'flash';
|
|
281
|
-
}, 3000);
|
|
287
|
+
flashTimer = setTimeout(() => { el.className = 'flash'; }, 3000);
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
// ── Toast Notifications ────────────────────────────────────────
|
|
285
291
|
function showToast(text, type) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
292
|
+
const container = document.getElementById('toast-container');
|
|
293
|
+
const toast = document.createElement('div');
|
|
294
|
+
const icons = { success: '\\u2713', error: '\\u2717', info: '\\u2139', warning: '\\u26a0' };
|
|
289
295
|
toast.className = 'toast ' + (type || 'info');
|
|
290
296
|
toast.innerHTML = '<span class="toast-icon">' + (icons[type] || icons.info) + '</span>' + escHtml(text);
|
|
291
297
|
container.appendChild(toast);
|
|
292
|
-
requestAnimationFrame(
|
|
293
|
-
requestAnimationFrame(
|
|
298
|
+
requestAnimationFrame(() => {
|
|
299
|
+
requestAnimationFrame(() => toast.classList.add('visible'));
|
|
294
300
|
});
|
|
295
|
-
setTimeout(
|
|
301
|
+
setTimeout(() => {
|
|
296
302
|
toast.classList.remove('visible');
|
|
297
|
-
setTimeout(
|
|
303
|
+
setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
|
|
298
304
|
}, 4000);
|
|
299
305
|
}
|
|
300
306
|
|
|
307
|
+
// ── Modal Helper ───────────────────────────────────────────────
|
|
308
|
+
// Reusable helper that manages show/hide, overlay-click-to-close,
|
|
309
|
+
// close-button click, and Escape key for standard modal overlays.
|
|
310
|
+
const _openModals = [];
|
|
311
|
+
|
|
312
|
+
function Modal(overlayId, closeId) {
|
|
313
|
+
this.overlay = document.getElementById(overlayId);
|
|
314
|
+
this.isOpen = false;
|
|
315
|
+
this.onHide = null;
|
|
316
|
+
if (closeId) {
|
|
317
|
+
const closeBtn = document.getElementById(closeId);
|
|
318
|
+
if (closeBtn) closeBtn.addEventListener('click', () => this.hide());
|
|
319
|
+
}
|
|
320
|
+
this.overlay.addEventListener('click', (e) => {
|
|
321
|
+
if (e.target === this.overlay) this.hide();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
Modal.prototype.show = function() {
|
|
326
|
+
this.isOpen = true;
|
|
327
|
+
this.overlay.className = 'modal-overlay active';
|
|
328
|
+
if (_openModals.indexOf(this) === -1) _openModals.push(this);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
Modal.prototype.hide = function() {
|
|
332
|
+
this.isOpen = false;
|
|
333
|
+
this.overlay.className = 'modal-overlay';
|
|
334
|
+
const idx = _openModals.indexOf(this);
|
|
335
|
+
if (idx !== -1) _openModals.splice(idx, 1);
|
|
336
|
+
if (this.onHide) this.onHide();
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
function anyModalOpen() {
|
|
340
|
+
return _openModals.length > 0 || confirmMode;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Create modal instances
|
|
344
|
+
const logViewerModal = new Modal('log-viewer-overlay', 'log-viewer-close');
|
|
345
|
+
const branchActionModal = new Modal('branch-action-overlay', 'branch-action-close');
|
|
346
|
+
const infoModal = new Modal('info-overlay', 'info-close');
|
|
347
|
+
const stashModal = new Modal('stash-overlay', 'stash-close');
|
|
348
|
+
const cleanupModal = new Modal('cleanup-overlay', 'cleanup-close');
|
|
349
|
+
const updateModal = new Modal('update-overlay', 'update-close');
|
|
350
|
+
|
|
351
|
+
// Per-modal hide callbacks for state cleanup
|
|
352
|
+
logViewerModal.onHide = () => { logViewerMode = false; };
|
|
353
|
+
branchActionModal.onHide = () => { branchActionMode = false; };
|
|
354
|
+
infoModal.onHide = () => { infoMode = false; };
|
|
355
|
+
stashModal.onHide = () => { stashMode = false; pendingStashBranch = null; };
|
|
356
|
+
cleanupModal.onHide = () => { cleanupMode = false; };
|
|
357
|
+
updateModal.onHide = () => { updateMode = false; };
|
|
358
|
+
|
|
301
359
|
// ── Confirm Dialog ─────────────────────────────────────────────
|
|
302
360
|
function showConfirm(title, message, onConfirm, opts) {
|
|
303
361
|
opts = opts || {};
|
|
304
362
|
confirmMode = true;
|
|
305
363
|
confirmCallback = onConfirm;
|
|
306
|
-
|
|
364
|
+
const box = document.getElementById('confirm-box');
|
|
307
365
|
box.innerHTML =
|
|
308
366
|
'<div class="confirm-title">' + escHtml(title) + '</div>' +
|
|
309
367
|
'<div class="confirm-message">' + escHtml(message) + '</div>' +
|
|
@@ -315,7 +373,7 @@ function getDashboardJs() {
|
|
|
315
373
|
'</div>';
|
|
316
374
|
document.getElementById('confirm-overlay').className = 'confirm-overlay active';
|
|
317
375
|
document.getElementById('confirm-cancel').onclick = hideConfirm;
|
|
318
|
-
document.getElementById('confirm-ok').onclick =
|
|
376
|
+
document.getElementById('confirm-ok').onclick = () => {
|
|
319
377
|
hideConfirm();
|
|
320
378
|
if (confirmCallback) confirmCallback();
|
|
321
379
|
};
|
|
@@ -329,19 +387,18 @@ function getDashboardJs() {
|
|
|
329
387
|
|
|
330
388
|
// ── Tabs ───────────────────────────────────────────────────────
|
|
331
389
|
function renderTabs() {
|
|
332
|
-
|
|
333
|
-
|
|
390
|
+
const tabBar = document.getElementById('tab-bar');
|
|
391
|
+
const projects = (state && state.projects) || [];
|
|
334
392
|
if (projects.length <= 1) {
|
|
335
393
|
tabBar.className = 'tab-bar';
|
|
336
394
|
return;
|
|
337
395
|
}
|
|
338
396
|
tabBar.className = 'tab-bar visible';
|
|
339
|
-
// Adjust layout height for tab bar
|
|
340
397
|
document.querySelector('.layout').style.height = 'calc(100vh - 49px - 40px)';
|
|
341
|
-
|
|
342
|
-
for (
|
|
343
|
-
|
|
344
|
-
|
|
398
|
+
let html = '';
|
|
399
|
+
for (let i = 0; i < projects.length; i++) {
|
|
400
|
+
const p = projects[i];
|
|
401
|
+
const isActive = p.id === activeTabId;
|
|
345
402
|
html += '<div class="tab' + (isActive ? ' active' : '') + '" data-project-id="' + escHtml(p.id) + '">';
|
|
346
403
|
html += '<span class="tab-dot"></span>';
|
|
347
404
|
html += escHtml(p.name);
|
|
@@ -351,17 +408,13 @@ function getDashboardJs() {
|
|
|
351
408
|
tabBar.innerHTML = html;
|
|
352
409
|
}
|
|
353
410
|
|
|
354
|
-
/**
|
|
355
|
-
* Fetch a project's state from the server and merge it into the
|
|
356
|
-
* current client-side state for rendering.
|
|
357
|
-
*/
|
|
358
411
|
function fetchAndApplyProjectState(projectId) {
|
|
359
|
-
|
|
412
|
+
const xhr = new XMLHttpRequest();
|
|
360
413
|
xhr.open('GET', '/api/projects/' + projectId + '/state');
|
|
361
|
-
xhr.onload =
|
|
414
|
+
xhr.onload = () => {
|
|
362
415
|
if (xhr.status === 200 && activeTabId === projectId) {
|
|
363
416
|
try {
|
|
364
|
-
|
|
417
|
+
const pState = JSON.parse(xhr.responseText);
|
|
365
418
|
state.branches = pState.branches || [];
|
|
366
419
|
state.currentBranch = pState.currentBranch;
|
|
367
420
|
state.activityLog = pState.activityLog || [];
|
|
@@ -392,126 +445,50 @@ function getDashboardJs() {
|
|
|
392
445
|
renderTabs();
|
|
393
446
|
fetchAndApplyProjectState(projectId);
|
|
394
447
|
|
|
395
|
-
// For non-local tabs the SSE stream won't push per-project updates,
|
|
396
|
-
// so poll the server periodically to keep the view fresh.
|
|
397
448
|
clearInterval(remoteTabPollTimer);
|
|
398
449
|
remoteTabPollTimer = null;
|
|
399
450
|
if (state && projectId !== state.activeProjectId) {
|
|
400
|
-
remoteTabPollTimer = setInterval(
|
|
451
|
+
remoteTabPollTimer = setInterval(() => {
|
|
401
452
|
fetchAndApplyProjectState(projectId);
|
|
402
453
|
}, 2000);
|
|
403
454
|
}
|
|
404
455
|
}
|
|
405
456
|
|
|
406
|
-
// ──
|
|
407
|
-
|
|
408
|
-
if (!dateStr) return '';
|
|
409
|
-
var ts = new Date(dateStr).getTime();
|
|
410
|
-
if (isNaN(ts)) return '';
|
|
411
|
-
var diff = Date.now() - ts;
|
|
412
|
-
if (diff < 0) return 'now';
|
|
413
|
-
var s = Math.floor(diff / 1000);
|
|
414
|
-
if (s < 60) return s + 's ago';
|
|
415
|
-
var m = Math.floor(s / 60);
|
|
416
|
-
if (m < 60) return m + 'm ago';
|
|
417
|
-
var h = Math.floor(m / 60);
|
|
418
|
-
if (h < 24) return h + 'h ago';
|
|
419
|
-
var d = Math.floor(h / 24);
|
|
420
|
-
return d + 'd ago';
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// ── Sparkline Rendering ────────────────────────────────────────
|
|
424
|
-
function renderSparklineBars(sparkStr) {
|
|
425
|
-
if (!sparkStr) return '';
|
|
426
|
-
var chars = '\\u2581\\u2582\\u2583\\u2584\\u2585\\u2586\\u2587\\u2588';
|
|
427
|
-
var html = '<div class="sparkline-bar">';
|
|
428
|
-
for (var i = 0; i < sparkStr.length; i++) {
|
|
429
|
-
var ch = sparkStr[i];
|
|
430
|
-
var idx = chars.indexOf(ch);
|
|
431
|
-
if (idx < 0) {
|
|
432
|
-
html += '<div class="spark-bar" style="height:1px"></div>';
|
|
433
|
-
} else {
|
|
434
|
-
var pct = Math.round(((idx + 1) / 8) * 100);
|
|
435
|
-
html += '<div class="spark-bar" style="height:' + pct + '%"></div>';
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
html += '</div>';
|
|
439
|
-
return html;
|
|
440
|
-
}
|
|
457
|
+
// ── Pure Utility Functions (inlined from pure.js) ──────────────
|
|
458
|
+
${pureFnBlock}
|
|
441
459
|
|
|
442
|
-
// ──
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return (n / 1000000).toFixed(1) + 'm';
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// ── Get Display Branches ───────────────────────────────────────
|
|
451
|
-
function getDisplayBranches() {
|
|
460
|
+
// ── Get Display Branches (wrapper) ─────────────────────────────
|
|
461
|
+
// The pure getDisplayBranches is inlined above as a var assignment.
|
|
462
|
+
// Wrap it to pass closure state as args, keeping the same call-site API.
|
|
463
|
+
const _pureGetDisplayBranches = getDisplayBranches;
|
|
464
|
+
getDisplayBranches = function() {
|
|
452
465
|
if (!state || !state.branches) return [];
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
// Pin branches to top
|
|
461
|
-
if (pinnedBranches.length > 0) {
|
|
462
|
-
var pinSet = {};
|
|
463
|
-
for (var i = 0; i < pinnedBranches.length; i++) pinSet[pinnedBranches[i]] = true;
|
|
464
|
-
branches.sort(function(a, b) {
|
|
465
|
-
var aPin = pinSet[a.name] ? 1 : 0;
|
|
466
|
-
var bPin = pinSet[b.name] ? 1 : 0;
|
|
467
|
-
return bPin - aPin; // pinned first
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
// Sort
|
|
471
|
-
if (sortOrder === 'alpha') {
|
|
472
|
-
var pinSet2 = {};
|
|
473
|
-
for (var j = 0; j < pinnedBranches.length; j++) pinSet2[pinnedBranches[j]] = true;
|
|
474
|
-
branches.sort(function(a, b) {
|
|
475
|
-
// Pinned branches always first
|
|
476
|
-
var aPin = pinSet2[a.name] ? 1 : 0;
|
|
477
|
-
var bPin = pinSet2[b.name] ? 1 : 0;
|
|
478
|
-
if (aPin !== bPin) return bPin - aPin;
|
|
479
|
-
return a.name.localeCompare(b.name);
|
|
480
|
-
});
|
|
481
|
-
} else if (sortOrder === 'recent') {
|
|
482
|
-
var pinSet3 = {};
|
|
483
|
-
for (var k = 0; k < pinnedBranches.length; k++) pinSet3[pinnedBranches[k]] = true;
|
|
484
|
-
branches.sort(function(a, b) {
|
|
485
|
-
var aPin = pinSet3[a.name] ? 1 : 0;
|
|
486
|
-
var bPin = pinSet3[b.name] ? 1 : 0;
|
|
487
|
-
if (aPin !== bPin) return bPin - aPin;
|
|
488
|
-
var aDate = a.date ? new Date(a.date).getTime() : 0;
|
|
489
|
-
var bDate = b.date ? new Date(b.date).getTime() : 0;
|
|
490
|
-
return bDate - aDate;
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
return branches;
|
|
494
|
-
}
|
|
466
|
+
return _pureGetDisplayBranches(state.branches, {
|
|
467
|
+
searchQuery: searchQuery,
|
|
468
|
+
pinnedBranches: pinnedBranches,
|
|
469
|
+
sortOrder: sortOrder,
|
|
470
|
+
});
|
|
471
|
+
};
|
|
495
472
|
|
|
496
473
|
// ── Render ─────────────────────────────────────────────────────
|
|
497
474
|
function render() {
|
|
498
475
|
if (!state) return;
|
|
499
476
|
|
|
500
477
|
// Header — hide project name pill when tabs are showing it
|
|
501
|
-
|
|
502
|
-
|
|
478
|
+
const projectEl = document.getElementById('project-name');
|
|
479
|
+
const hasTabs = state.projects && state.projects.length > 1;
|
|
503
480
|
if (hasTabs) {
|
|
504
481
|
projectEl.style.display = 'none';
|
|
505
482
|
} else {
|
|
506
483
|
projectEl.style.display = '';
|
|
507
484
|
projectEl.textContent = state.projectName || '-';
|
|
508
485
|
}
|
|
509
|
-
|
|
486
|
+
const versionEl = document.getElementById('version');
|
|
510
487
|
if (state.version) versionEl.textContent = 'v' + state.version;
|
|
511
488
|
|
|
512
489
|
// Status badge
|
|
513
490
|
if (connected) {
|
|
514
|
-
|
|
491
|
+
const badge = document.getElementById('status-badge');
|
|
515
492
|
if (state.isOffline) {
|
|
516
493
|
badge.className = 'badge badge-offline';
|
|
517
494
|
badge.textContent = 'offline';
|
|
@@ -540,9 +517,9 @@ function getDashboardJs() {
|
|
|
540
517
|
}
|
|
541
518
|
|
|
542
519
|
function renderBranches() {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
520
|
+
const container = document.getElementById('branch-list');
|
|
521
|
+
const branches = getDisplayBranches();
|
|
522
|
+
const countEl = document.getElementById('branch-count');
|
|
546
523
|
countEl.textContent = branches.length;
|
|
547
524
|
|
|
548
525
|
if (selectedIndex >= branches.length) {
|
|
@@ -557,23 +534,23 @@ function getDashboardJs() {
|
|
|
557
534
|
return;
|
|
558
535
|
}
|
|
559
536
|
|
|
560
|
-
|
|
561
|
-
for (
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
537
|
+
let html = '';
|
|
538
|
+
for (let i = 0; i < branches.length; i++) {
|
|
539
|
+
const b = branches[i];
|
|
540
|
+
const isSelected = i === selectedIndex;
|
|
541
|
+
const isCurrent = b.name === state.currentBranch;
|
|
565
542
|
|
|
566
543
|
// Sparkline
|
|
567
|
-
|
|
544
|
+
const sparkStr = state.sparklineCache ? state.sparklineCache[b.name] : null;
|
|
568
545
|
|
|
569
546
|
// PR status
|
|
570
|
-
|
|
571
|
-
|
|
547
|
+
const prStatus = state.branchPrStatusMap ? state.branchPrStatusMap[b.name] : null;
|
|
548
|
+
const isMerged = prStatus && prStatus.state === 'MERGED';
|
|
572
549
|
|
|
573
550
|
// Ahead/behind
|
|
574
|
-
|
|
551
|
+
const ab = state.aheadBehindCache ? state.aheadBehindCache[b.name] : null;
|
|
575
552
|
|
|
576
|
-
|
|
553
|
+
let itemClasses = 'branch-item';
|
|
577
554
|
if (isSelected) itemClasses += ' selected';
|
|
578
555
|
if (isCurrent) itemClasses += ' current';
|
|
579
556
|
if (isMerged) itemClasses += ' merged';
|
|
@@ -586,8 +563,8 @@ function getDashboardJs() {
|
|
|
586
563
|
html += '<div class="branch-info">';
|
|
587
564
|
html += '<div class="branch-name-row">';
|
|
588
565
|
// Branch name - clickable link to GitHub/GitLab
|
|
589
|
-
|
|
590
|
-
|
|
566
|
+
const branchUrl = getBranchUrl(b.name);
|
|
567
|
+
const isPinned = pinnedBranches.indexOf(b.name) !== -1;
|
|
591
568
|
html += '<span class="branch-name">';
|
|
592
569
|
if (branchUrl) {
|
|
593
570
|
html += '<a href="' + escHtml(branchUrl) + '" target="_blank" rel="noopener" title="Open on web" onclick="event.stopPropagation()">' + escHtml(b.name) + '</a>';
|
|
@@ -601,7 +578,7 @@ function getDashboardJs() {
|
|
|
601
578
|
|
|
602
579
|
html += '<div class="branch-meta">';
|
|
603
580
|
// Commit hash - clickable link
|
|
604
|
-
|
|
581
|
+
const commitUrl = getCommitUrl(b.commit);
|
|
605
582
|
html += '<span class="branch-commit">';
|
|
606
583
|
if (commitUrl) {
|
|
607
584
|
html += '<a href="' + escHtml(commitUrl) + '" target="_blank" rel="noopener" title="View commit" onclick="event.stopPropagation()">' + escHtml(b.commit || '') + '</a>';
|
|
@@ -619,15 +596,15 @@ function getDashboardJs() {
|
|
|
619
596
|
|
|
620
597
|
html += '<div class="branch-right">';
|
|
621
598
|
// Badges
|
|
622
|
-
|
|
599
|
+
let badges = '';
|
|
623
600
|
if (isCurrent) badges += '<span class="branch-current-badge">HEAD</span>';
|
|
624
601
|
if (isPinned) badges += '<span class="branch-new-badge" style="color:var(--orange);background:rgba(219,109,40,0.15)">pinned</span>';
|
|
625
602
|
if (b.isNew) badges += '<span class="branch-new-badge">new</span>';
|
|
626
603
|
if (b.isDeleted) badges += '<span class="branch-deleted-badge">deleted</span>';
|
|
627
604
|
if (b.justUpdated) badges += '<span class="branch-updated-badge">updated</span>';
|
|
628
605
|
if (prStatus) {
|
|
629
|
-
|
|
630
|
-
|
|
606
|
+
const prClass = prStatus.state === 'OPEN' ? 'pr-open' : prStatus.state === 'MERGED' ? 'pr-merged' : 'pr-closed';
|
|
607
|
+
const prUrl = getPrUrl(prStatus.number);
|
|
631
608
|
badges += '<span class="pr-badge ' + prClass + '">';
|
|
632
609
|
if (prUrl) badges += '<a href="' + escHtml(prUrl) + '" target="_blank" rel="noopener" onclick="event.stopPropagation()">';
|
|
633
610
|
badges += (prStatus.state === 'MERGED' ? 'merged' : 'PR #' + prStatus.number);
|
|
@@ -659,25 +636,25 @@ function getDashboardJs() {
|
|
|
659
636
|
container.innerHTML = html;
|
|
660
637
|
|
|
661
638
|
// Scroll selected into view
|
|
662
|
-
|
|
639
|
+
const selected = container.querySelector('.branch-item.selected');
|
|
663
640
|
if (selected) {
|
|
664
641
|
selected.scrollIntoView({ block: 'nearest' });
|
|
665
642
|
}
|
|
666
643
|
}
|
|
667
644
|
|
|
668
645
|
function renderActivityLog() {
|
|
669
|
-
|
|
670
|
-
|
|
646
|
+
const container = document.getElementById('activity-log');
|
|
647
|
+
const log = (state && state.activityLog) || [];
|
|
671
648
|
if (log.length === 0) {
|
|
672
649
|
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📋</div>No activity yet</div>';
|
|
673
650
|
return;
|
|
674
651
|
}
|
|
675
|
-
|
|
676
|
-
for (
|
|
677
|
-
|
|
678
|
-
|
|
652
|
+
let html = '';
|
|
653
|
+
for (let i = 0; i < log.length; i++) {
|
|
654
|
+
const entry = log[i];
|
|
655
|
+
const t = '';
|
|
679
656
|
if (entry.timestamp) {
|
|
680
|
-
|
|
657
|
+
const d = new Date(entry.timestamp);
|
|
681
658
|
t = isNaN(d.getTime()) ? '' : d.toLocaleTimeString();
|
|
682
659
|
}
|
|
683
660
|
html += '<div class="log-entry">';
|
|
@@ -694,31 +671,28 @@ function getDashboardJs() {
|
|
|
694
671
|
logViewerMode = true;
|
|
695
672
|
logViewerTab = 'server';
|
|
696
673
|
renderLogViewer();
|
|
697
|
-
|
|
674
|
+
logViewerModal.show();
|
|
698
675
|
}
|
|
699
676
|
|
|
700
|
-
function hideLogViewer() {
|
|
701
|
-
logViewerMode = false;
|
|
702
|
-
document.getElementById('log-viewer-overlay').className = 'modal-overlay';
|
|
703
|
-
}
|
|
677
|
+
function hideLogViewer() { logViewerModal.hide(); }
|
|
704
678
|
|
|
705
679
|
function renderLogViewer() {
|
|
706
680
|
if (!state) return;
|
|
707
|
-
|
|
681
|
+
const container = document.getElementById('log-viewer-content');
|
|
708
682
|
// Update tab active state
|
|
709
|
-
|
|
710
|
-
for (
|
|
683
|
+
const tabs = document.querySelectorAll('.log-viewer-tab');
|
|
684
|
+
for (let t = 0; t < tabs.length; t++) {
|
|
711
685
|
tabs[t].className = 'log-viewer-tab' + (tabs[t].getAttribute('data-tab') === logViewerTab ? ' active' : '');
|
|
712
686
|
}
|
|
713
687
|
|
|
714
|
-
|
|
688
|
+
let html = '';
|
|
715
689
|
if (logViewerTab === 'server') {
|
|
716
|
-
|
|
690
|
+
const logs = state.serverLogBuffer || [];
|
|
717
691
|
if (logs.length === 0) {
|
|
718
692
|
html = '<div style="color:var(--text-muted);padding:20px;text-align:center;">No server logs</div>';
|
|
719
693
|
} else {
|
|
720
|
-
for (
|
|
721
|
-
|
|
694
|
+
for (let i = 0; i < logs.length; i++) {
|
|
695
|
+
const log = logs[i];
|
|
722
696
|
html += '<div class="log-line' + (log.isError ? ' error' : '') + '">';
|
|
723
697
|
html += '<span class="log-ts">' + escHtml(log.timestamp || '') + '</span>';
|
|
724
698
|
html += escHtml(log.line || '');
|
|
@@ -726,13 +700,13 @@ function getDashboardJs() {
|
|
|
726
700
|
}
|
|
727
701
|
}
|
|
728
702
|
} else {
|
|
729
|
-
|
|
703
|
+
const alog = (state.activityLog || []);
|
|
730
704
|
if (alog.length === 0) {
|
|
731
705
|
html = '<div style="color:var(--text-muted);padding:20px;text-align:center;">No activity</div>';
|
|
732
706
|
} else {
|
|
733
|
-
for (
|
|
734
|
-
|
|
735
|
-
|
|
707
|
+
for (let j = 0; j < alog.length; j++) {
|
|
708
|
+
const entry = alog[j];
|
|
709
|
+
const ts = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '';
|
|
736
710
|
html += '<div class="log-line">';
|
|
737
711
|
html += '<span class="log-ts">' + ts + '</span>';
|
|
738
712
|
html += escHtml(entry.message || '');
|
|
@@ -744,33 +718,29 @@ function getDashboardJs() {
|
|
|
744
718
|
container.scrollTop = container.scrollHeight;
|
|
745
719
|
}
|
|
746
720
|
|
|
747
|
-
document.getElementById('log-viewer-tabs').addEventListener('click',
|
|
748
|
-
|
|
721
|
+
document.getElementById('log-viewer-tabs').addEventListener('click', (e) => {
|
|
722
|
+
const tab = e.target.closest('.log-viewer-tab');
|
|
749
723
|
if (!tab) return;
|
|
750
724
|
logViewerTab = tab.getAttribute('data-tab');
|
|
751
725
|
renderLogViewer();
|
|
752
726
|
});
|
|
753
727
|
|
|
754
|
-
document.getElementById('log-viewer-close').addEventListener('click', hideLogViewer);
|
|
755
|
-
document.getElementById('log-viewer-overlay').addEventListener('click', function(e) {
|
|
756
|
-
if (e.target === this) hideLogViewer();
|
|
757
|
-
});
|
|
758
|
-
|
|
759
728
|
// ── Branch Action Modal ────────────────────────────────────────
|
|
760
729
|
function showBranchActions() {
|
|
761
|
-
|
|
730
|
+
const branches = getDisplayBranches();
|
|
762
731
|
if (!branches.length || selectedIndex >= branches.length) return;
|
|
763
|
-
|
|
732
|
+
const branch = branches[selectedIndex];
|
|
764
733
|
branchActionMode = true;
|
|
734
|
+
branchActionModal.show();
|
|
765
735
|
document.getElementById('branch-action-title').textContent = 'Actions: ' + branch.name;
|
|
766
736
|
|
|
767
|
-
|
|
768
|
-
|
|
737
|
+
const prStatus = (state.branchPrStatusMap || {})[branch.name];
|
|
738
|
+
const isCurrent = branch.name === state.currentBranch;
|
|
769
739
|
|
|
770
|
-
|
|
740
|
+
const actions = [];
|
|
771
741
|
|
|
772
742
|
// Open on web (GitHub/GitLab) — direct link if we have repo URL
|
|
773
|
-
|
|
743
|
+
const brUrl = getBranchUrl(branch.name);
|
|
774
744
|
if (brUrl) {
|
|
775
745
|
actions.push({ icon: '\\u{1f310}', label: 'Open branch on web', key: 'openLink', data: { url: brUrl } });
|
|
776
746
|
} else {
|
|
@@ -778,7 +748,7 @@ function getDashboardJs() {
|
|
|
778
748
|
}
|
|
779
749
|
|
|
780
750
|
// PR actions
|
|
781
|
-
|
|
751
|
+
const prUrl = prStatus ? getPrUrl(prStatus.number) : null;
|
|
782
752
|
if (prStatus && prUrl) {
|
|
783
753
|
actions.push({ icon: '\\u{1f517}', label: 'View PR #' + prStatus.number, key: 'openLink', data: { url: prUrl } });
|
|
784
754
|
} else if (prStatus && prStatus.url) {
|
|
@@ -795,7 +765,7 @@ function getDashboardJs() {
|
|
|
795
765
|
}
|
|
796
766
|
|
|
797
767
|
// Pin/Unpin
|
|
798
|
-
|
|
768
|
+
const isPinnedBranch = pinnedBranches.indexOf(branch.name) !== -1;
|
|
799
769
|
actions.push({ icon: isPinnedBranch ? '\\u{1f4cc}' : '\\u{1f4cc}', label: isPinnedBranch ? 'Unpin branch' : 'Pin branch to top', key: 'pin', data: { branch: branch.name } });
|
|
800
770
|
|
|
801
771
|
// Switch to branch
|
|
@@ -811,33 +781,24 @@ function getDashboardJs() {
|
|
|
811
781
|
// Fetch
|
|
812
782
|
actions.push({ icon: '\\u{1f504}', label: 'Fetch all remotes', key: 'fetch', data: {} });
|
|
813
783
|
|
|
814
|
-
|
|
815
|
-
for (
|
|
816
|
-
|
|
784
|
+
let html = '';
|
|
785
|
+
for (let i = 0; i < actions.length; i++) {
|
|
786
|
+
const a = actions[i];
|
|
817
787
|
html += '<button class="action-item" data-action-key="' + escHtml(a.key) + '" data-action-data=\\'' + escHtml(JSON.stringify(a.data)) + '\\'>';
|
|
818
788
|
html += '<span class="action-icon">' + a.icon + '</span>';
|
|
819
789
|
html += '<span class="action-label">' + escHtml(a.label) + '</span>';
|
|
820
790
|
html += '</button>';
|
|
821
791
|
}
|
|
822
792
|
document.getElementById('branch-action-list').innerHTML = html;
|
|
823
|
-
document.getElementById('branch-action-overlay').className = 'modal-overlay active';
|
|
824
793
|
}
|
|
825
794
|
|
|
826
|
-
function hideBranchActions() {
|
|
827
|
-
branchActionMode = false;
|
|
828
|
-
document.getElementById('branch-action-overlay').className = 'modal-overlay';
|
|
829
|
-
}
|
|
795
|
+
function hideBranchActions() { branchActionModal.hide(); }
|
|
830
796
|
|
|
831
|
-
document.getElementById('branch-action-
|
|
832
|
-
|
|
833
|
-
if (e.target === this) hideBranchActions();
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
document.getElementById('branch-action-list').addEventListener('click', function(e) {
|
|
837
|
-
var btn = e.target.closest('.action-item');
|
|
797
|
+
document.getElementById('branch-action-list').addEventListener('click', (e) => {
|
|
798
|
+
const btn = e.target.closest('.action-item');
|
|
838
799
|
if (!btn) return;
|
|
839
|
-
|
|
840
|
-
|
|
800
|
+
const key = btn.getAttribute('data-action-key');
|
|
801
|
+
const data = {};
|
|
841
802
|
try { data = JSON.parse(btn.getAttribute('data-action-data') || '{}'); } catch (err) { /* ignore */ }
|
|
842
803
|
|
|
843
804
|
hideBranchActions();
|
|
@@ -849,7 +810,7 @@ function getDashboardJs() {
|
|
|
849
810
|
} else if (key === 'copy') {
|
|
850
811
|
copyToClipboard(data.text, null);
|
|
851
812
|
} else if (key === 'pin') {
|
|
852
|
-
|
|
813
|
+
const pIdx = pinnedBranches.indexOf(data.branch);
|
|
853
814
|
if (pIdx === -1) {
|
|
854
815
|
pinnedBranches.push(data.branch);
|
|
855
816
|
showToast('Pinned: ' + data.branch, 'success');
|
|
@@ -876,8 +837,8 @@ function getDashboardJs() {
|
|
|
876
837
|
function showInfo() {
|
|
877
838
|
if (!state) return;
|
|
878
839
|
infoMode = true;
|
|
879
|
-
|
|
880
|
-
|
|
840
|
+
const grid = document.getElementById('info-grid');
|
|
841
|
+
const rows = [
|
|
881
842
|
['Project', state.projectName || '-'],
|
|
882
843
|
['Version', 'v' + (state.version || '-')],
|
|
883
844
|
['Server Mode', state.serverMode || 'none'],
|
|
@@ -889,71 +850,54 @@ function getDashboardJs() {
|
|
|
889
850
|
['Network', state.isOffline ? 'Offline' : 'Online'],
|
|
890
851
|
['Branches', String((state.branches || []).length)],
|
|
891
852
|
];
|
|
892
|
-
|
|
893
|
-
for (
|
|
853
|
+
let html = '';
|
|
854
|
+
for (let i = 0; i < rows.length; i++) {
|
|
894
855
|
html += '<span class="info-label">' + escHtml(rows[i][0]) + '</span>';
|
|
895
856
|
html += '<span class="info-value">' + escHtml(rows[i][1]) + '</span>';
|
|
896
857
|
}
|
|
897
858
|
grid.innerHTML = html;
|
|
898
|
-
|
|
859
|
+
infoModal.show();
|
|
899
860
|
}
|
|
900
861
|
|
|
901
|
-
function hideInfo() {
|
|
902
|
-
infoMode = false;
|
|
903
|
-
document.getElementById('info-overlay').className = 'modal-overlay';
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
document.getElementById('info-close').addEventListener('click', hideInfo);
|
|
907
|
-
document.getElementById('info-overlay').addEventListener('click', function(e) {
|
|
908
|
-
if (e.target === this) hideInfo();
|
|
909
|
-
});
|
|
862
|
+
function hideInfo() { infoModal.hide(); }
|
|
910
863
|
|
|
911
864
|
// ── Stash Management ───────────────────────────────────────────
|
|
912
865
|
function showStashDialog(pendingBranch) {
|
|
913
866
|
stashMode = true;
|
|
914
867
|
pendingStashBranch = pendingBranch || null;
|
|
915
|
-
|
|
868
|
+
const msg = pendingBranch
|
|
916
869
|
? 'You have uncommitted changes. Stash them before switching to <strong>' + escHtml(pendingBranch) + '</strong>?'
|
|
917
870
|
: 'Stash all uncommitted changes in the working directory?';
|
|
918
|
-
|
|
871
|
+
const html = '<div style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">' + msg + '</div>';
|
|
919
872
|
html += '<div class="confirm-actions">';
|
|
920
873
|
html += '<button class="confirm-btn" id="stash-cancel">Cancel</button>';
|
|
921
874
|
html += '<button class="confirm-btn primary" id="stash-confirm">Stash & Continue</button>';
|
|
922
875
|
html += '</div>';
|
|
923
876
|
document.getElementById('stash-content').innerHTML = html;
|
|
924
|
-
|
|
877
|
+
stashModal.show();
|
|
925
878
|
document.getElementById('stash-cancel').onclick = hideStash;
|
|
926
|
-
document.getElementById('stash-confirm').onclick =
|
|
879
|
+
document.getElementById('stash-confirm').onclick = () => {
|
|
927
880
|
sendAction('stash', { pendingBranch: pendingStashBranch });
|
|
928
881
|
showToast('Stashing changes...', 'info');
|
|
929
882
|
hideStash();
|
|
930
883
|
};
|
|
931
884
|
}
|
|
932
885
|
|
|
933
|
-
function hideStash() {
|
|
934
|
-
stashMode = false;
|
|
935
|
-
pendingStashBranch = null;
|
|
936
|
-
document.getElementById('stash-overlay').className = 'modal-overlay';
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
document.getElementById('stash-close').addEventListener('click', hideStash);
|
|
940
|
-
document.getElementById('stash-overlay').addEventListener('click', function(e) {
|
|
941
|
-
if (e.target === this) hideStash();
|
|
942
|
-
});
|
|
886
|
+
function hideStash() { stashModal.hide(); }
|
|
943
887
|
|
|
944
888
|
// ── Branch Cleanup ─────────────────────────────────────────────
|
|
945
889
|
function showCleanup() {
|
|
946
890
|
cleanupMode = true;
|
|
947
|
-
|
|
891
|
+
const html = '<div style="color:var(--text-dim);font-size:13px;margin-bottom:12px;">Scanning for branches with deleted remotes...</div>';
|
|
948
892
|
document.getElementById('cleanup-content').innerHTML = html;
|
|
949
|
-
|
|
893
|
+
cleanupModal.show();
|
|
950
894
|
|
|
951
895
|
// Ask the server to find gone branches (we inspect state.branches for gone tracking hints)
|
|
952
896
|
// For now, look at branches that have no remote
|
|
953
|
-
|
|
897
|
+
const goneBranches = [];
|
|
954
898
|
if (state && state.branches) {
|
|
955
|
-
for (
|
|
956
|
-
|
|
899
|
+
for (let i = 0; i < state.branches.length; i++) {
|
|
900
|
+
const b = state.branches[i];
|
|
957
901
|
if (b.isLocal && !b.hasRemote && b.name !== state.currentBranch) {
|
|
958
902
|
goneBranches.push(b.name);
|
|
959
903
|
}
|
|
@@ -970,7 +914,7 @@ function getDashboardJs() {
|
|
|
970
914
|
|
|
971
915
|
html = '<div style="color:var(--text-dim);font-size:13px;margin-bottom:8px;">Found ' + goneBranches.length + ' branch(es) with no remote tracking:</div>';
|
|
972
916
|
html += '<div class="cleanup-branch-list">';
|
|
973
|
-
for (
|
|
917
|
+
for (let j = 0; j < goneBranches.length; j++) {
|
|
974
918
|
html += '<div class="cleanup-branch-item"><span class="cleanup-branch-icon">✖</span>' + escHtml(goneBranches[j]) + '</div>';
|
|
975
919
|
}
|
|
976
920
|
html += '</div>';
|
|
@@ -982,16 +926,16 @@ function getDashboardJs() {
|
|
|
982
926
|
|
|
983
927
|
document.getElementById('cleanup-content').innerHTML = html;
|
|
984
928
|
document.getElementById('cleanup-cancel').onclick = hideCleanup;
|
|
985
|
-
document.getElementById('cleanup-safe').onclick =
|
|
929
|
+
document.getElementById('cleanup-safe').onclick = () => {
|
|
986
930
|
sendAction('deleteBranches', { branches: goneBranches, force: false });
|
|
987
931
|
showToast('Deleting ' + goneBranches.length + ' branches (safe)...', 'info');
|
|
988
932
|
hideCleanup();
|
|
989
933
|
};
|
|
990
|
-
document.getElementById('cleanup-force').onclick =
|
|
934
|
+
document.getElementById('cleanup-force').onclick = () => {
|
|
991
935
|
showConfirm(
|
|
992
936
|
'Force Delete',
|
|
993
937
|
'Force delete ' + goneBranches.length + ' branch(es)? This may delete unmerged work.',
|
|
994
|
-
|
|
938
|
+
() => {
|
|
995
939
|
sendAction('deleteBranches', { branches: goneBranches, force: true });
|
|
996
940
|
showToast('Force deleting ' + goneBranches.length + ' branches...', 'warning');
|
|
997
941
|
hideCleanup();
|
|
@@ -1001,21 +945,13 @@ function getDashboardJs() {
|
|
|
1001
945
|
};
|
|
1002
946
|
}
|
|
1003
947
|
|
|
1004
|
-
function hideCleanup() {
|
|
1005
|
-
cleanupMode = false;
|
|
1006
|
-
document.getElementById('cleanup-overlay').className = 'modal-overlay';
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
document.getElementById('cleanup-close').addEventListener('click', hideCleanup);
|
|
1010
|
-
document.getElementById('cleanup-overlay').addEventListener('click', function(e) {
|
|
1011
|
-
if (e.target === this) hideCleanup();
|
|
1012
|
-
});
|
|
948
|
+
function hideCleanup() { cleanupModal.hide(); }
|
|
1013
949
|
|
|
1014
950
|
// ── Update Notification ────────────────────────────────────────
|
|
1015
951
|
function showUpdateModal() {
|
|
1016
952
|
if (!state || !state.updateAvailable) return;
|
|
1017
953
|
updateMode = true;
|
|
1018
|
-
|
|
954
|
+
const html = '<div class="update-versions">';
|
|
1019
955
|
html += '<span class="old-version">v' + escHtml(state.version || '?') + '</span>';
|
|
1020
956
|
html += '<span class="arrow">→</span>';
|
|
1021
957
|
html += '<span class="new-version">v' + escHtml(state.updateAvailable) + '</span>';
|
|
@@ -1030,10 +966,10 @@ function getDashboardJs() {
|
|
|
1030
966
|
html += '</div>';
|
|
1031
967
|
}
|
|
1032
968
|
document.getElementById('update-content').innerHTML = html;
|
|
1033
|
-
|
|
969
|
+
updateModal.show();
|
|
1034
970
|
if (!state.updateInProgress) {
|
|
1035
971
|
document.getElementById('update-dismiss').onclick = hideUpdate;
|
|
1036
|
-
document.getElementById('update-install').onclick =
|
|
972
|
+
document.getElementById('update-install').onclick = () => {
|
|
1037
973
|
sendAction('checkUpdate', { install: true });
|
|
1038
974
|
showToast('Installing update...', 'info');
|
|
1039
975
|
hideUpdate();
|
|
@@ -1041,26 +977,18 @@ function getDashboardJs() {
|
|
|
1041
977
|
}
|
|
1042
978
|
}
|
|
1043
979
|
|
|
1044
|
-
function hideUpdate() {
|
|
1045
|
-
updateMode = false;
|
|
1046
|
-
document.getElementById('update-overlay').className = 'modal-overlay';
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
document.getElementById('update-close').addEventListener('click', hideUpdate);
|
|
1050
|
-
document.getElementById('update-overlay').addEventListener('click', function(e) {
|
|
1051
|
-
if (e.target === this) hideUpdate();
|
|
1052
|
-
});
|
|
980
|
+
function hideUpdate() { updateModal.hide(); }
|
|
1053
981
|
|
|
1054
982
|
// ── Session Stats ──────────────────────────────────────────────
|
|
1055
983
|
function renderSessionStats() {
|
|
1056
984
|
if (!state || !state.sessionStats) return;
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
985
|
+
const s = state.sessionStats;
|
|
986
|
+
const bar = document.getElementById('stats-bar');
|
|
987
|
+
const activeBranches = 0;
|
|
988
|
+
const staleBranches = 0;
|
|
1061
989
|
if (state.branches) {
|
|
1062
|
-
for (
|
|
1063
|
-
|
|
990
|
+
for (let i = 0; i < state.branches.length; i++) {
|
|
991
|
+
const b = state.branches[i];
|
|
1064
992
|
// Consider stale if no updates and not current
|
|
1065
993
|
if (b.justUpdated || b.name === state.currentBranch) {
|
|
1066
994
|
activeBranches++;
|
|
@@ -1069,7 +997,7 @@ function getDashboardJs() {
|
|
|
1069
997
|
}
|
|
1070
998
|
}
|
|
1071
999
|
}
|
|
1072
|
-
|
|
1000
|
+
let html = '';
|
|
1073
1001
|
html += '<span class="stat-item"><span class="stat-label">Session:</span> <span class="stat-value">' + escHtml(s.sessionDuration || '0m') + '</span></span>';
|
|
1074
1002
|
html += '<span class="stat-item"><span class="stat-label">Lines:</span> <span class="stat-value">+' + (s.linesAdded || 0) + '/-' + (s.linesDeleted || 0) + '</span></span>';
|
|
1075
1003
|
html += '<span class="stat-item"><span class="stat-label">Polls:</span> <span class="stat-value">' + (s.totalPolls || 0) + '</span> <span class="stat-label">(' + (s.hitRate || 0) + '% hit)</span></span>';
|
|
@@ -1082,56 +1010,50 @@ function getDashboardJs() {
|
|
|
1082
1010
|
|
|
1083
1011
|
// ── Error Toast with Stash Hint ────────────────────────────────
|
|
1084
1012
|
function showErrorToastWithHint(message, hint) {
|
|
1085
|
-
|
|
1086
|
-
|
|
1013
|
+
const container = document.getElementById('toast-container');
|
|
1014
|
+
const toast = document.createElement('div');
|
|
1087
1015
|
toast.className = 'toast error';
|
|
1088
|
-
|
|
1016
|
+
const html = '<span class="toast-icon">\\u2717</span>' + escHtml(message);
|
|
1089
1017
|
if (hint) {
|
|
1090
1018
|
html += '<span class="toast-action" data-hint="' + escHtml(hint) + '">' + escHtml(hint) + '</span>';
|
|
1091
1019
|
}
|
|
1092
1020
|
toast.innerHTML = html;
|
|
1093
1021
|
container.appendChild(toast);
|
|
1094
|
-
requestAnimationFrame(
|
|
1095
|
-
requestAnimationFrame(
|
|
1022
|
+
requestAnimationFrame(() => {
|
|
1023
|
+
requestAnimationFrame(() => toast.classList.add('visible'));
|
|
1096
1024
|
});
|
|
1097
1025
|
|
|
1098
1026
|
// Handle hint click
|
|
1099
|
-
|
|
1027
|
+
const hintEl = toast.querySelector('.toast-action');
|
|
1100
1028
|
if (hintEl) {
|
|
1101
|
-
hintEl.addEventListener('click',
|
|
1102
|
-
|
|
1029
|
+
hintEl.addEventListener('click', (e) => {
|
|
1030
|
+
const h = e.currentTarget.getAttribute('data-hint');
|
|
1103
1031
|
if (h === 'Press S to stash') {
|
|
1104
1032
|
showStashDialog(pendingStashBranch);
|
|
1105
1033
|
}
|
|
1106
1034
|
toast.classList.remove('visible');
|
|
1107
|
-
setTimeout(
|
|
1035
|
+
setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
|
|
1108
1036
|
});
|
|
1109
1037
|
}
|
|
1110
1038
|
|
|
1111
|
-
setTimeout(
|
|
1039
|
+
setTimeout(() => {
|
|
1112
1040
|
toast.classList.remove('visible');
|
|
1113
|
-
setTimeout(
|
|
1041
|
+
setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
|
|
1114
1042
|
}, 6000);
|
|
1115
1043
|
}
|
|
1116
1044
|
|
|
1117
|
-
// ── Any modal open check ───────────────────────────────────────
|
|
1118
|
-
function anyModalOpen() {
|
|
1119
|
-
return logViewerMode || branchActionMode || infoMode || cleanupMode || updateMode || stashMode || confirmMode;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
1045
|
// ── Keyboard ───────────────────────────────────────────────────
|
|
1123
|
-
document.addEventListener('keydown',
|
|
1046
|
+
document.addEventListener('keydown', (e) => {
|
|
1124
1047
|
// Ignore when typing in input fields (other than search)
|
|
1125
1048
|
if (e.target.tagName === 'INPUT' && e.target.id !== 'search-input') return;
|
|
1126
1049
|
if (e.target.tagName === 'BUTTON') return;
|
|
1127
1050
|
|
|
1128
|
-
// Any modal — Escape to close
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
if (stashMode && e.key === 'Escape') { e.preventDefault(); hideStash(); return; }
|
|
1051
|
+
// Any modal — Escape to close the topmost one
|
|
1052
|
+
if (_openModals.length > 0 && e.key === 'Escape') {
|
|
1053
|
+
e.preventDefault();
|
|
1054
|
+
_openModals[_openModals.length - 1].hide();
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1135
1057
|
|
|
1136
1058
|
// Log viewer tab switching
|
|
1137
1059
|
if (logViewerMode) {
|
|
@@ -1144,14 +1066,14 @@ function getDashboardJs() {
|
|
|
1144
1066
|
}
|
|
1145
1067
|
|
|
1146
1068
|
// Block other keys while modals are open
|
|
1147
|
-
if (
|
|
1069
|
+
if (_openModals.length > 0) return;
|
|
1148
1070
|
|
|
1149
1071
|
// Confirm dialog mode — Escape to cancel, Enter to confirm
|
|
1150
1072
|
if (confirmMode) {
|
|
1151
1073
|
if (e.key === 'Escape') { e.preventDefault(); hideConfirm(); }
|
|
1152
1074
|
if (e.key === 'Enter') {
|
|
1153
1075
|
e.preventDefault();
|
|
1154
|
-
|
|
1076
|
+
const cb = confirmCallback;
|
|
1155
1077
|
hideConfirm();
|
|
1156
1078
|
if (cb) cb();
|
|
1157
1079
|
}
|
|
@@ -1190,9 +1112,9 @@ function getDashboardJs() {
|
|
|
1190
1112
|
}
|
|
1191
1113
|
|
|
1192
1114
|
// Tab switching with number keys (1-9)
|
|
1193
|
-
|
|
1115
|
+
const projects = (state && state.projects) || [];
|
|
1194
1116
|
if (projects.length > 1 && e.key >= '1' && e.key <= '9') {
|
|
1195
|
-
|
|
1117
|
+
const tabIdx = parseInt(e.key, 10) - 1;
|
|
1196
1118
|
if (tabIdx < projects.length) {
|
|
1197
1119
|
e.preventDefault();
|
|
1198
1120
|
switchTab(projects[tabIdx].id);
|
|
@@ -1203,8 +1125,8 @@ function getDashboardJs() {
|
|
|
1203
1125
|
// Tab cycling with Tab key
|
|
1204
1126
|
if (e.key === 'Tab' && projects.length > 1) {
|
|
1205
1127
|
e.preventDefault();
|
|
1206
|
-
|
|
1207
|
-
|
|
1128
|
+
const curIdx = projects.findIndex((p) => p.id === activeTabId);
|
|
1129
|
+
const nextIdx = e.shiftKey
|
|
1208
1130
|
? (curIdx - 1 + projects.length) % projects.length
|
|
1209
1131
|
: (curIdx + 1) % projects.length;
|
|
1210
1132
|
switchTab(projects[nextIdx].id);
|
|
@@ -1225,9 +1147,9 @@ function getDashboardJs() {
|
|
|
1225
1147
|
break;
|
|
1226
1148
|
case 'Enter':
|
|
1227
1149
|
e.preventDefault();
|
|
1228
|
-
|
|
1150
|
+
const branches = getDisplayBranches();
|
|
1229
1151
|
if (branches.length > 0 && selectedIndex < branches.length) {
|
|
1230
|
-
|
|
1152
|
+
const b = branches[selectedIndex];
|
|
1231
1153
|
if (b.isDeleted) {
|
|
1232
1154
|
showToast('Cannot switch to a deleted branch', 'error');
|
|
1233
1155
|
} else if (b.name === state.currentBranch) {
|
|
@@ -1244,7 +1166,7 @@ function getDashboardJs() {
|
|
|
1244
1166
|
searchQuery = '';
|
|
1245
1167
|
selectedIndex = 0;
|
|
1246
1168
|
document.getElementById('search-bar').className = 'search-bar active';
|
|
1247
|
-
|
|
1169
|
+
const input = document.getElementById('search-input');
|
|
1248
1170
|
input.value = '';
|
|
1249
1171
|
input.focus();
|
|
1250
1172
|
break;
|
|
@@ -1271,7 +1193,7 @@ function getDashboardJs() {
|
|
|
1271
1193
|
showConfirm(
|
|
1272
1194
|
'Restart Server',
|
|
1273
1195
|
'Restart the dev server process?',
|
|
1274
|
-
|
|
1196
|
+
() => {
|
|
1275
1197
|
sendAction('restartServer');
|
|
1276
1198
|
showToast('Restarting server...', 'info');
|
|
1277
1199
|
},
|
|
@@ -1291,8 +1213,8 @@ function getDashboardJs() {
|
|
|
1291
1213
|
case 'h':
|
|
1292
1214
|
e.preventDefault();
|
|
1293
1215
|
if (state && state.switchHistory && state.switchHistory.length > 0) {
|
|
1294
|
-
|
|
1295
|
-
|
|
1216
|
+
const last = state.switchHistory[0];
|
|
1217
|
+
const histMsg = 'Last: ' + last.from + ' \\u2192 ' + last.to;
|
|
1296
1218
|
if (state.switchHistory.length > 1) histMsg += ' (+' + (state.switchHistory.length - 1) + ' more)';
|
|
1297
1219
|
showToast(histMsg, 'info');
|
|
1298
1220
|
} else {
|
|
@@ -1336,15 +1258,15 @@ function getDashboardJs() {
|
|
|
1336
1258
|
});
|
|
1337
1259
|
|
|
1338
1260
|
// Search input handler
|
|
1339
|
-
document.getElementById('search-input').addEventListener('input',
|
|
1261
|
+
document.getElementById('search-input').addEventListener('input', (e) => {
|
|
1340
1262
|
searchQuery = e.target.value;
|
|
1341
1263
|
selectedIndex = 0;
|
|
1342
1264
|
renderBranches();
|
|
1343
1265
|
});
|
|
1344
1266
|
|
|
1345
1267
|
function moveSelection(delta) {
|
|
1346
|
-
|
|
1347
|
-
|
|
1268
|
+
const branches = getDisplayBranches();
|
|
1269
|
+
const newIndex = selectedIndex + delta;
|
|
1348
1270
|
if (newIndex >= 0 && newIndex < branches.length) {
|
|
1349
1271
|
selectedIndex = newIndex;
|
|
1350
1272
|
renderBranches();
|
|
@@ -1352,18 +1274,18 @@ function getDashboardJs() {
|
|
|
1352
1274
|
}
|
|
1353
1275
|
|
|
1354
1276
|
// ── Click Handlers ─────────────────────────────────────────────
|
|
1355
|
-
document.getElementById('branch-list').addEventListener('click',
|
|
1356
|
-
|
|
1277
|
+
document.getElementById('branch-list').addEventListener('click', (e) => {
|
|
1278
|
+
const item = e.target.closest('.branch-item');
|
|
1357
1279
|
if (!item) return;
|
|
1358
|
-
|
|
1280
|
+
const idx = parseInt(item.getAttribute('data-index'), 10);
|
|
1359
1281
|
if (isNaN(idx)) return;
|
|
1360
1282
|
selectedIndex = idx;
|
|
1361
1283
|
renderBranches();
|
|
1362
1284
|
|
|
1363
1285
|
// Double-click to switch with confirmation
|
|
1364
1286
|
if (e.detail === 2) {
|
|
1365
|
-
|
|
1366
|
-
|
|
1287
|
+
const branches = getDisplayBranches();
|
|
1288
|
+
const br = branches[idx];
|
|
1367
1289
|
if (br && !br.isDeleted && br.name !== state.currentBranch) {
|
|
1368
1290
|
sendAction('switchBranch', { branch: br.name });
|
|
1369
1291
|
showToast('Switching to ' + br.name + '...', 'info');
|
|
@@ -1371,25 +1293,25 @@ function getDashboardJs() {
|
|
|
1371
1293
|
}
|
|
1372
1294
|
});
|
|
1373
1295
|
|
|
1374
|
-
document.getElementById('confirm-overlay').addEventListener('click',
|
|
1296
|
+
document.getElementById('confirm-overlay').addEventListener('click', (e) => {
|
|
1375
1297
|
if (e.target === this) hideConfirm();
|
|
1376
1298
|
});
|
|
1377
1299
|
|
|
1378
1300
|
// Tab clicks
|
|
1379
|
-
document.getElementById('tab-bar').addEventListener('click',
|
|
1380
|
-
|
|
1301
|
+
document.getElementById('tab-bar').addEventListener('click', (e) => {
|
|
1302
|
+
const tab = e.target.closest('.tab');
|
|
1381
1303
|
if (!tab) return;
|
|
1382
|
-
|
|
1304
|
+
const projectId = tab.getAttribute('data-project-id');
|
|
1383
1305
|
if (projectId) switchTab(projectId);
|
|
1384
1306
|
});
|
|
1385
1307
|
|
|
1386
1308
|
// ── Preferences Bar ─────────────────────────────────────────────
|
|
1387
1309
|
function renderPrefsBar() {
|
|
1388
1310
|
// Insert prefs controls into footer if not already there
|
|
1389
|
-
|
|
1390
|
-
|
|
1311
|
+
const footer = document.getElementById('footer');
|
|
1312
|
+
const existing = document.getElementById('prefs-bar');
|
|
1391
1313
|
if (!existing) {
|
|
1392
|
-
|
|
1314
|
+
const div = document.createElement('span');
|
|
1393
1315
|
div.id = 'prefs-bar';
|
|
1394
1316
|
div.style.cssText = 'display:flex;gap:6px;align-items:center;margin-left:auto;';
|
|
1395
1317
|
div.innerHTML =
|
|
@@ -1403,23 +1325,23 @@ function getDashboardJs() {
|
|
|
1403
1325
|
}
|
|
1404
1326
|
|
|
1405
1327
|
// Prefs bar click handler
|
|
1406
|
-
document.getElementById('footer').addEventListener('click',
|
|
1407
|
-
|
|
1328
|
+
document.getElementById('footer').addEventListener('click', (e) => {
|
|
1329
|
+
const sortBtn = e.target.closest('[data-sort]');
|
|
1408
1330
|
if (sortBtn) {
|
|
1409
1331
|
sortOrder = sortBtn.getAttribute('data-sort');
|
|
1410
1332
|
savePrefs({ sortOrder: sortOrder });
|
|
1411
|
-
|
|
1412
|
-
for (
|
|
1333
|
+
const sortBtns = document.querySelectorAll('[data-sort]');
|
|
1334
|
+
for (let i = 0; i < sortBtns.length; i++) {
|
|
1413
1335
|
sortBtns[i].className = 'pref-btn' + (sortBtns[i].getAttribute('data-sort') === sortOrder ? ' active' : '');
|
|
1414
1336
|
}
|
|
1415
1337
|
renderBranches();
|
|
1416
1338
|
return;
|
|
1417
1339
|
}
|
|
1418
1340
|
if (e.target.id === 'pin-selected-btn') {
|
|
1419
|
-
|
|
1341
|
+
const branches = getDisplayBranches();
|
|
1420
1342
|
if (branches.length > 0 && selectedIndex < branches.length) {
|
|
1421
|
-
|
|
1422
|
-
|
|
1343
|
+
const bn = branches[selectedIndex].name;
|
|
1344
|
+
const idx = pinnedBranches.indexOf(bn);
|
|
1423
1345
|
if (idx === -1) {
|
|
1424
1346
|
pinnedBranches.push(bn);
|
|
1425
1347
|
showToast('Pinned: ' + bn, 'success');
|
|
@@ -1435,7 +1357,7 @@ function getDashboardJs() {
|
|
|
1435
1357
|
if (e.target.id === 'toggle-sidebar-btn') {
|
|
1436
1358
|
sidebarCollapsed = !sidebarCollapsed;
|
|
1437
1359
|
savePrefs({ sidebarCollapsed: sidebarCollapsed });
|
|
1438
|
-
|
|
1360
|
+
const layout = document.querySelector('.layout');
|
|
1439
1361
|
if (sidebarCollapsed) {
|
|
1440
1362
|
layout.classList.add('sidebar-collapsed');
|
|
1441
1363
|
} else {
|
|
@@ -1447,20 +1369,20 @@ function getDashboardJs() {
|
|
|
1447
1369
|
});
|
|
1448
1370
|
|
|
1449
1371
|
// ── Sidebar Toggle (header) ───────────────────────────────────
|
|
1450
|
-
document.getElementById('sidebar-toggle').addEventListener('click',
|
|
1372
|
+
document.getElementById('sidebar-toggle').addEventListener('click', () => {
|
|
1451
1373
|
sidebarCollapsed = !sidebarCollapsed;
|
|
1452
1374
|
savePrefs({ sidebarCollapsed: sidebarCollapsed });
|
|
1453
|
-
|
|
1375
|
+
const layout = document.querySelector('.layout');
|
|
1454
1376
|
layout.classList.toggle('sidebar-collapsed', sidebarCollapsed);
|
|
1455
|
-
|
|
1377
|
+
const btn = document.getElementById('toggle-sidebar-btn');
|
|
1456
1378
|
if (btn) btn.className = 'pref-btn' + (sidebarCollapsed ? ' active' : '');
|
|
1457
1379
|
});
|
|
1458
1380
|
|
|
1459
1381
|
// ── Copy button delegation ────────────────────────────────────
|
|
1460
|
-
document.addEventListener('click',
|
|
1461
|
-
|
|
1382
|
+
document.addEventListener('click', (e) => {
|
|
1383
|
+
const copyBtn = e.target.closest('.copy-btn');
|
|
1462
1384
|
if (!copyBtn) return;
|
|
1463
|
-
|
|
1385
|
+
const text = copyBtn.getAttribute('data-copy');
|
|
1464
1386
|
if (text) {
|
|
1465
1387
|
e.preventDefault();
|
|
1466
1388
|
e.stopPropagation();
|
|
@@ -1468,12 +1390,6 @@ function getDashboardJs() {
|
|
|
1468
1390
|
}
|
|
1469
1391
|
});
|
|
1470
1392
|
|
|
1471
|
-
// ── Utility ────────────────────────────────────────────────────
|
|
1472
|
-
function escHtml(s) {
|
|
1473
|
-
if (!s) return '';
|
|
1474
|
-
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
1393
|
// ── Init ───────────────────────────────────────────────────────
|
|
1478
1394
|
connect();
|
|
1479
1395
|
})();
|