Update panel: always show version status (up-to-date or update available)

Replace dismissable toast with persistent status row showing a green
dot + "Up to date" with commit hash, or amber dot + "Update available"
with an inline Install button. One or the other is always visible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
roberts 2026-03-14 21:49:37 -05:00
parent eb1aa9d434
commit 436a009059
2 changed files with 65 additions and 60 deletions

View file

@ -213,74 +213,72 @@
color: var(--accent); color: var(--accent);
} }
/* Update toast */ /* Update status */
.panel-updates { .panel-updates {
transition: all var(--transition-base); transition: all var(--transition-base);
} }
.update-toast { .update-status {
position: relative;
padding: 12px;
background: var(--accent-dim);
border-left: 3px solid var(--accent);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.update-dismiss {
position: absolute;
top: 4px;
right: 4px;
width: 24px;
height: 24px;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 10px;
border-radius: var(--radius-sm);
} }
.update-dismiss:hover { .update-status-dot {
background: var(--accent-hover); width: 8px;
color: var(--text-primary); height: 8px;
border-radius: 50%;
flex-shrink: 0;
} }
.update-toast-title { .update-status-dot.current {
background: var(--status-green);
}
.update-status-dot.available {
background: var(--status-amber);
}
.update-status-title {
font-family: var(--font-ui); font-family: var(--font-ui);
font-size: 13px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: var(--accent); color: var(--text-primary);
margin-bottom: 4px; line-height: 1.2;
} }
.update-toast-info { .update-status-hash {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 11px; font-size: 10px;
color: var(--text-secondary); color: var(--text-muted);
margin-bottom: 10px; line-height: 1.3;
} }
.update-toast-btn { .update-status-text {
width: 100%; flex: 1;
height: 30px; min-width: 0;
}
.update-install-btn {
flex-shrink: 0;
height: 26px;
padding: 0 12px;
background: var(--accent); background: var(--accent);
border: none; border: none;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: #fff; color: #fff;
font-family: var(--font-ui); font-family: var(--font-ui);
font-size: 12px; font-size: 11px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
white-space: nowrap;
} }
.update-toast-btn:hover { .update-install-btn:hover {
opacity: 0.9; opacity: 0.9;
} }
.update-toast-btn:disabled { .update-install-btn:disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
} }

View file

@ -391,42 +391,50 @@
// ===================================================================== // =====================================================================
// Panel — Update checker // Panel — Update checker
// ===================================================================== // =====================================================================
let updateDismissed = false;
async function checkForUpdates() { async function checkForUpdates() {
if (updateDismissed) return; const panel = $('#panelUpdates');
if (!panel) return;
try { try {
const res = await Atlus.apiFetch('/api/updates/check'); const res = await Atlus.apiFetch('/api/updates/check');
if (!res || !res.ok) return; if (!res || !res.ok) return;
const data = await res.json(); const data = await res.json();
const panel = $('#panelUpdates');
if (!panel) return; panel.classList.remove('hidden');
if (data.available && data.behind_count > 0) { if (data.available && data.behind_count > 0) {
showUpdateToast(panel, data); showUpdateAvailable(panel, data);
} else { } else {
panel.classList.add('hidden'); showUpToDate(panel, data);
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
} }
function showUpdateToast(panel, data) { function showUpToDate(panel, data) {
panel.classList.remove('hidden');
panel.innerHTML = ` panel.innerHTML = `
<div class="update-toast"> <div class="update-status update-current">
<button class="update-dismiss" title="Dismiss">&times;</button> <span class="update-status-dot current"></span>
<div class="update-toast-title">Update available</div> <div class="update-status-text">
<div class="update-toast-info">${data.behind_count} commit${data.behind_count !== 1 ? 's' : ''} behind (${data.remote_hash})</div> <div class="update-status-title">Up to date</div>
<button class="update-toast-btn">Install Update</button> <div class="update-status-hash">${data.local_hash}</div>
</div>
</div>
`;
}
function showUpdateAvailable(panel, data) {
panel.innerHTML = `
<div class="update-status update-available">
<span class="update-status-dot available"></span>
<div class="update-status-text">
<div class="update-status-title">Update available</div>
<div class="update-status-hash">${data.behind_count} commit${data.behind_count !== 1 ? 's' : ''} behind (${data.remote_hash})</div>
</div>
<button class="update-install-btn" title="Install update">Update</button>
</div> </div>
`; `;
panel.querySelector('.update-dismiss').addEventListener('click', () => { panel.querySelector('.update-install-btn').addEventListener('click', async (e) => {
panel.classList.add('hidden');
updateDismissed = true;
});
panel.querySelector('.update-toast-btn').addEventListener('click', async (e) => {
const btn = e.target; const btn = e.target;
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Updating…'; btn.textContent = 'Updating…';
@ -435,10 +443,9 @@
const res = await Atlus.apiFetch('/api/updates/apply', { method: 'POST' }); const res = await Atlus.apiFetch('/api/updates/apply', { method: 'POST' });
if (res.ok) { if (res.ok) {
btn.textContent = 'Restarting…'; btn.textContent = 'Restarting…';
// Server will restart — try to reload after a delay
setTimeout(() => attemptReload(0), 4000); setTimeout(() => attemptReload(0), 4000);
} else { } else {
btn.textContent = 'Update failed'; btn.textContent = 'Failed';
btn.disabled = false; btn.disabled = false;
} }
} catch (e) { } catch (e) {