+
+
'; + result.innerHTML = + '
0
Machines
-
+
0
Repositories
-
+
';
return html;
@@ -1262,55 +1751,17 @@
0
Clean
@@ -826,7 +1200,7 @@ Connect your machines
← Back to overview
';
}
var pills = '' + escapeHtml(r.account || '') + '';
- if (showMachine) pills += '\uD83D\uDCBB ' + escapeHtml(r.machineName || r.machineId || '') + '';
+ if (showMachine) pills += '' + escapeHtml(r.machineName || r.machineId || '') + '';
html += '';
html += '
-
@@ -959,8 +1346,17 @@ —
+Loading…
@@ -837,7 +1211,7 @@
-
+
—
—
if (name === 'overview') document.getElementById('navOverview').classList.add('nav-active'); if (name === 'repos') document.getElementById('navRepos').classList.add('nav-active'); if (name === 'settings') document.getElementById('navSettings').classList.add('nav-active'); + var pathLabel = name === 'machineDetail' ? 'machine' : name; + var brandPath = document.getElementById('brandPath'); + var panelPath = document.getElementById('mainPanelPath'); + if (brandPath) brandPath.textContent = 'hub.gitdock.dev / ' + pathLabel; + if (panelPath) panelPath.textContent = pathLabel; if (name === 'overview') loadOverview(); - if (name === 'repos') loadAllRepos(); + if (name === 'repos') { + var reposCountEl = document.getElementById('allReposCount'); + if (reposCountEl && !allReposData.length) reposCountEl.textContent = 'Loading…'; + loadAllRepos(); + } if (name === 'settings') loadKeys(); } @@ -997,7 +1393,11 @@—
var emailEl = document.getElementById('userEmail'); if (emailEl) emailEl.textContent = me.email || ''; document.getElementById('loginView').style.display = 'none'; - document.getElementById('appView').style.display = 'block'; + document.getElementById('loginView').setAttribute('aria-hidden', 'true'); + var appEl = document.getElementById('appView'); + appEl.style.display = 'flex'; + appEl.classList.add('app-visible'); + appEl.removeAttribute('aria-hidden'); showView('overview'); lastUpdatedAt = Date.now(); if (lastUpdatedInterval) clearInterval(lastUpdatedInterval); @@ -1014,12 +1414,20 @@—
if (lastUpdatedInterval) clearInterval(lastUpdatedInterval); lastUpdatedInterval = null; document.getElementById('loginView').style.display = 'flex'; - document.getElementById('appView').style.display = 'none'; + document.getElementById('loginView').removeAttribute('aria-hidden'); + var appHide = document.getElementById('appView'); + appHide.style.display = 'none'; + appHide.classList.remove('app-visible'); + appHide.setAttribute('aria-hidden', 'true'); } }).catch(function() { csrfToken = null; document.getElementById('loginView').style.display = 'flex'; - document.getElementById('appView').style.display = 'none'; + document.getElementById('loginView').removeAttribute('aria-hidden'); + var appHide2 = document.getElementById('appView'); + appHide2.style.display = 'none'; + appHide2.classList.remove('app-visible'); + appHide2.setAttribute('aria-hidden', 'true'); }); } @@ -1106,12 +1514,12 @@—
var totalClean = totalRepos - totalAttention; var now = Date.now(); var onlineCount = machines.filter(function(m) { - var t = m.last_seen ? new Date(m.last_seen).getTime() : 0; - return (now - t) <= STALE_MS; + var t = parseHubDate(m.last_seen); + return t && (now - t.getTime()) <= STALE_MS; }).length; var staleCount = machines.length - onlineCount; document.getElementById('statMachines').textContent = machines.length; - document.getElementById('statMachinesMeta').textContent = machines.length ? (onlineCount + ' online' + (staleCount ? ', ' + staleCount + ' stale' : '')) : '—'; + document.getElementById('statMachinesMeta').textContent = machines.length ? (onlineCount + ' online' + (staleCount ? ', ' + staleCount + ' stale' : '')) : 'n/a'; document.getElementById('statRepos').textContent = totalRepos; document.getElementById('statClean').textContent = totalClean; document.getElementById('statAttention').textContent = totalAttention; @@ -1149,11 +1557,17 @@—
var card = document.createElement('div'); card.className = 'machine-card'; card.dataset.id = m.id; - var lastSeen = m.last_seen ? new Date(m.last_seen).toLocaleString() : '—'; - card.innerHTML = '' + escapeHtml(m.name || m.id) + '
' + - '' + - '' + (m.repo_count || 0) + ' repos' + (m.issue_count || 0) + ' issues
';
- card.addEventListener('click', function() { openMachineDetail(m.id); });
+ var lastSeen = formatHubTime(m.last_seen);
+ var seenAt = parseHubDate(m.last_seen);
+ var isOnline = seenAt && (Date.now() - seenAt.getTime() < 60 * 60 * 1000);
+ card.innerHTML =
+ '' +
+ '' +
+ '
';
+ card.addEventListener('click', function() { openMachineDetail(m.id, m); });
grid.appendChild(card);
});
}
@@ -1207,20 +1621,95 @@ ' + escapeHtml(m.name || m.id) + '
' + + '' + + '' + (m.repo_count || 0) + ' repos' + (m.issue_count || 0) + ' issues
' +
+ '—
} function buildStatHtml(label, abbr, value, cls) { - var hot = value > 0 ? ' rc-stat--hot' : ''; - return '' + + return '' + '' + abbr + '' + '' + value + ''; } + function buildActiveCountersHtml(d) { + var parts = []; + if (d.staged) parts.push(buildStatHtml('Staged', 'S', d.staged, 'staged')); + if (d.unstaged) parts.push(buildStatHtml('Modified', 'M', d.unstaged, 'modified')); + if (d.untracked) parts.push(buildStatHtml('Untracked', '?', d.untracked, 'untracked')); + if (d.conflict) parts.push(buildStatHtml('Conflicts', '!', d.conflict, 'conflict')); + return parts.length ? '' + parts.join('') + '
' : '';
+ }
+
+ function buildSyncHtml(d) {
+ if (!(d.ahead > 0 || d.behind > 0)) return '';
+ var syncHtml = '';
+ if (d.ahead > 0) syncHtml += '\u2191' + d.ahead + ' ahead';
+ if (d.behind > 0) syncHtml += '\u2193' + d.behind + ' behind';
+ syncHtml += '
';
+ return syncHtml;
+ }
+
+ function buildRepoCardHtml(r, d, opts) {
+ opts = opts || {};
+ var showMachine = !!opts.showMachine;
+ var dateTitle = d.absoluteDate ? ' title="' + escapeHtml(d.absoluteDate) + '"' : '';
+ var subBits = [];
+ if (d.timeAgo) subBits.push('' + escapeHtml(d.timeAgo) + '');
+ if (showMachine && (r.machineName || r.machineId)) {
+ subBits.push('' + escapeHtml(r.machineName || r.machineId) + '');
+ }
+ if (r.multiMachineCount > 1) {
+ subBits.push('' + r.multiMachineCount + ' machines');
+ }
+ var leftFoot = buildActiveCountersHtml(d) + buildSyncHtml(d);
+ return '' +
+ '
' +
+ buildCardDetailHtml(r, d, showMachine);
+ }
+
+ function buildRepoRowHtml(r, d, opts) {
+ opts = opts || {};
+ var showMachine = !!opts.showMachine;
+ var dateTitle = d.absoluteDate ? ' title="' + escapeHtml(d.absoluteDate) + '"' : '';
+ var signals = buildActiveCountersHtml(d) + buildSyncHtml(d);
+ var meta = [];
+ if (showMachine && (r.machineName || r.machineId)) {
+ meta.push('' + escapeHtml(r.machineName || r.machineId) + '');
+ }
+ if (r.multiMachineCount > 1) {
+ meta.push('' + r.multiMachineCount + ' machines');
+ }
+ if (r.account) meta.push('' + escapeHtml(r.account) + '');
+ if (d.timeAgo) meta.push('' + escapeHtml(d.timeAgo) + '');
+ return '' +
+ '' +
+ '' + escapeHtml(r.name) + '' +
+ '' + escapeHtml(r.branch || 'n/a') + '' +
+ 'details' +
+ '
' +
+ (subBits.length ? '' + subBits.join('') + '
' : '') +
+ '' +
+ '
' +
+ '' + leftFoot + '
' +
+ '' + d.status + '' +
+ '' +
+ '' +
+ '' + escapeHtml(r.name) + '' +
+ '' + escapeHtml(r.branch || 'n/a') + '' +
+ '
' +
+ '' +
+ signals +
+ '' + d.status + '' +
+ '
' +
+ (meta.length ? '' : '');
+ }
+
function buildCardDetailHtml(r, d, showMachine) {
var html = '';
if (d.msg || d.author) {
html += '';
}
+ if (d.dirtyText && d.status === 'dirty') {
+ html += '';
+ }
if (d.ahead > 0 || d.behind > 0) {
var total = d.ahead + d.behind;
var aPct = Math.round((d.ahead / total) * 100);
@@ -1229,7 +1718,7 @@
Last commit
' +
(d.msg ? '' + escapeHtml(d.msg) + '
' : '') +
- (d.author ? '' : '') +
+ (d.author ? '' : '') +
'Working tree
' + escapeHtml(d.dirtyText) + '
—
'' + pills + '
—
emptyEl.style.display = 'none'; filtered.forEach(function(r) { var d = buildRepoData(r); - var status = d.status, ahead = d.ahead, behind = d.behind, msg = d.msg, timeAgo = d.timeAgo, author = d.author, absoluteDate = d.absoluteDate, dirtyLine = d.dirtyLine, dirtyText = d.dirtyText, aheadBehind = d.aheadBehind, aheadBehindBar = d.aheadBehindBar, needsAttention = d.needsAttention; - var dateTitle = absoluteDate ? ' title="' + escapeHtml(absoluteDate) + '"' : ''; + var needsAttention = d.needsAttention; if (machineDetailViewMode === 'cards') { var card = document.createElement('li'); card.className = 'repo-card ' + (needsAttention ? 'rc--attention' : 'rc--clean'); - var counters = buildStatHtml('Staged', 'S', d.staged, 'staged') + - buildStatHtml('Modified', 'M', d.unstaged, 'modified') + - buildStatHtml('Untracked', '?', d.untracked, 'untracked') + - buildStatHtml('Conflicts', '!', d.conflict, 'conflict'); - var syncHtml = ''; - if (ahead > 0 || behind > 0) { - syncHtml = '';
- if (ahead > 0) syncHtml += '\u2191' + ahead + ' ahead';
- if (behind > 0) syncHtml += '\u2193' + behind + ' behind';
- syncHtml += '
';
- }
- card.innerHTML =
- '' +
- '
' +
- buildCardDetailHtml(r, d, false);
+ card.innerHTML = buildRepoCardHtml(r, d, { showMachine: false });
card.addEventListener('click', function() { card.classList.toggle('rc-expanded'); });
list.appendChild(card);
} else {
var row = document.createElement('li');
- row.className = 'repo-row' + (needsAttention ? ' repo-row-needs-attention' : '');
- row.innerHTML =
- '' +
- '' +
- '' + escapeHtml(r.name) + '' +
- '' + escapeHtml(r.branch || '\u2014') + '' +
- '\u25B8' +
- '
' +
- '' +
- (timeAgo ? '' + escapeHtml(timeAgo) + '' : '') +
- '
' +
- '' +
- '
' +
- syncHtml +
- '' + counters + '
' +
- '' + status + '' +
- '' +
- '' + escapeHtml(r.name) + '' +
- '' + escapeHtml(r.account || '') + '' +
- '' + escapeHtml(r.branch || '—') + '' +
- (aheadBehind || '') +
- '' + status + '' +
- '
' +
- (author ? '' : '') +
- (msg || timeAgo || dirtyLine ? '' : '');
+ row.className = 'repo-row ' + (needsAttention ? 'rr--attention' : 'rr--clean');
+ row.innerHTML = buildRepoRowHtml(r, d, { showMachine: false });
list.appendChild(row);
}
});
@@ -1364,85 +1815,47 @@ —
return; } emptyEl.style.display = 'none'; - var keyToRepos = {}; - allReposData.forEach(function(x) { - var k = repoKey(x); - if (!keyToRepos[k]) keyToRepos[k] = []; - keyToRepos[k].push(x); - }); filtered.forEach(function(r) { var d = buildRepoData(r); - var status = d.status, ahead = d.ahead, behind = d.behind, msg = d.msg, timeAgo = d.timeAgo, author = d.author, absoluteDate = d.absoluteDate, dirtyLine = d.dirtyLine, dirtyText = d.dirtyText, aheadBehind = d.aheadBehind, aheadBehindBar = d.aheadBehindBar, needsAttention = d.needsAttention; - var dateTitle = absoluteDate ? ' title="' + escapeHtml(absoluteDate) + '"' : ''; - var sameKeyRepos = keyToRepos[repoKey(r)] || []; - var statusPerMachine = (r.multiMachineCount > 1 && sameKeyRepos.length > 1) ? sameKeyRepos.map(function(x) { return escapeHtml(x.machineName || '') + ': ' + (x.localStatus || 'clean'); }).join(', ') : ''; + var needsAttention = d.needsAttention; if (allReposViewMode === 'cards') { var card = document.createElement('li'); card.className = 'repo-card ' + (needsAttention ? 'rc--attention' : 'rc--clean'); - var counters = buildStatHtml('Staged', 'S', d.staged, 'staged') + - buildStatHtml('Modified', 'M', d.unstaged, 'modified') + - buildStatHtml('Untracked', '?', d.untracked, 'untracked') + - buildStatHtml('Conflicts', '!', d.conflict, 'conflict'); - var syncHtml = ''; - if (ahead > 0 || behind > 0) { - syncHtml = '';
- if (ahead > 0) syncHtml += '\u2191' + ahead + ' ahead';
- if (behind > 0) syncHtml += '\u2193' + behind + ' behind';
- syncHtml += '
';
- }
- card.innerHTML =
- '' +
- '
' +
- buildCardDetailHtml(r, d, true);
+ card.innerHTML = buildRepoCardHtml(r, d, { showMachine: true });
card.addEventListener('click', function() { card.classList.toggle('rc-expanded'); });
list.appendChild(card);
} else {
var row = document.createElement('li');
- row.className = 'repo-row' + (needsAttention ? ' repo-row-needs-attention' : '');
- row.innerHTML =
- '' +
- '' +
- '' + escapeHtml(r.name) + '' +
- '' + escapeHtml(r.branch || '\u2014') + '' +
- '\u25B8' +
- '
' +
- '' +
- (timeAgo ? '' + escapeHtml(timeAgo) + '' : '') +
- '' + escapeHtml(r.machineName || r.machineId || '') + '' +
- (r.multiMachineCount > 1 ? 'on ' + r.multiMachineCount + ' machines' : '') +
- '
' +
- '' +
- '
' +
- syncHtml +
- '' + counters + '
' +
- '' + status + '' +
- '' +
- '' + escapeHtml(r.name) + '' +
- '' + escapeHtml(r.account || '') + '' +
- '' + escapeHtml(r.branch || '—') + '' +
- '' + escapeHtml(r.machineName || '') + '' +
- (aheadBehind || '') +
- '' + status + '' +
- '
' +
- (author ? '' : '') +
- (msg || timeAgo || dirtyLine ? '' : '');
+ row.className = 'repo-row ' + (needsAttention ? 'rr--attention' : 'rr--clean');
+ row.innerHTML = buildRepoRowHtml(r, d, { showMachine: true });
list.appendChild(row);
}
});
}
- function openMachineDetail(id) {
+ function openMachineDetail(id, hint) {
currentMachineDetailId = id;
document.getElementById('machineDetailRenameWrap').style.display = 'none';
- showView('machineDetail');
document.getElementById('machineDetailSearch').value = '';
document.querySelectorAll('.machine-detail-filters .fbtn').forEach(function(b) { b.classList.toggle('on', b.getAttribute('data-filter') === 'all'); });
machineDetailFilter = 'all';
+ var nameEl = document.getElementById('machineDetailName');
+ var metaEl = document.getElementById('machineDetailMeta');
+ var countEl = document.getElementById('machineDetailRepoCount');
+ if (hint && hint.name) nameEl.textContent = hint.name;
+ else if (!machineDetailRepos.length) nameEl.textContent = 'Loading…';
+ if (hint && hint.platform) {
+ metaEl.textContent = hint.platform + (hint.last_seen ? ' \u2022 Last seen: ' + formatHubTime(hint.last_seen) : '');
+ } else if (!machineDetailRepos.length) {
+ metaEl.textContent = 'Loading…';
+ }
+ if (countEl && !machineDetailRepos.length) countEl.textContent = 'Loading…';
+ showView('machineDetail');
apiGet('/api/machines/' + encodeURIComponent(id)).then(function(data) {
var m = data.machine;
var snap = data.snapshot;
- document.getElementById('machineDetailName').textContent = m.name || m.id;
- document.getElementById('machineDetailMeta').textContent = (m.platform || '') + ' \u2022 Last seen: ' + (data.received_at ? new Date(data.received_at).toLocaleString() : '—');
+ nameEl.textContent = m.name || m.id;
+ metaEl.textContent = (m.platform || '') + ' \u2022 Last seen: ' + formatHubTime(data.received_at || m.last_seen);
machineDetailRepos = (snap && snap.repos) ? snap.repos : [];
renderMachineDetailRepos();
});
@@ -1536,14 +1949,18 @@ —
apiGet('/api/keys').then(function(data) { var keys = data.keys || []; var list = document.getElementById('keysList'); + var emptyEl = document.getElementById('keysEmpty'); list.innerHTML = ''; + if (emptyEl) emptyEl.style.display = keys.length ? 'none' : 'block'; + var keysCount = document.getElementById('keysCount'); + if (keysCount) keysCount.textContent = String(keys.length); keys.forEach(function(k) { var li = document.createElement('li'); li.className = 'key-row'; - li.innerHTML = '' + escapeHtml(k.label) + '
' +
+ li.innerHTML = '' + escapeHtml(k.label) + '
' +
'';
li.querySelector('button').addEventListener('click', function() {
- if (confirm('Revoke this key? Agents using it will no longer be able to send snapshots.')) {
+ if (confirm('Revoke this key? Machines using it will stop sending snapshots until you create a new key and update them.')) {
apiDelete('/api/keys/' + encodeURIComponent(k.id)).then(function() { loadKeys(); });
}
});
@@ -1557,12 +1974,20 @@ —
apiPost('/api/keys', { label: label }).then(function(data) { var result = document.getElementById('newKeyResult'); result.style.display = 'block'; - result.innerHTML = 'New API key (copy now \u2014 it won\'t be shown again):' + escapeHtml(data.key) + ''; + result.innerHTML = + '
Copy this key now. It will not be shown again. Use the same key on every machine.
' + + '' + escapeHtml(data.key) + '' +
+ '';
result.querySelector('.copy-btn').addEventListener('click', function() {
- navigator.clipboard.writeText(data.key);
- this.textContent = 'Copied';
+ var btn = this;
+ navigator.clipboard.writeText(data.key).then(function() {
+ btn.textContent = 'Copied';
+ }).catch(function() {
+ btn.textContent = 'Select and copy manually';
+ });
});
document.getElementById('newKeyLabel').value = '';
+ loadKeys();
});
});
@@ -1573,6 +1998,29 @@ 