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