feat(ui): persist active tab in localStorage and update release info in-place without page reload
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m43s

This commit is contained in:
2026-10-04 01:25:48 +02:00
parent 02bc65698b
commit 285360b59f
+86 -16
View File
@@ -658,7 +658,7 @@ class WebUIRenderer:
<div class="card-title" style="font-size: 15px;">🌐 Server Version & Upstream Release Status</div> <div class="card-title" style="font-size: 15px;">🌐 Server Version & Upstream Release Status</div>
<div class="subtitle" style="margin-top: 4px;">Upstream release tracking via Gitea production channels</div> <div class="subtitle" style="margin-top: 4px;">Upstream release tracking via Gitea production channels</div>
</div> </div>
{('<span class="badge badge-warning">UPDATE AVAILABLE</span>' if update_status.get("server_update_available") else '<span class="badge badge-active">UP TO DATE</span>')} <span id="upstreamReleaseStatusBadge">{('<span class="badge badge-warning">UPDATE AVAILABLE</span>' if update_status.get("server_update_available") else '<span class="badge badge-active">UP TO DATE</span>')}</span>
</div> </div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 20px;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 20px;">
<div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid var(--border);"> <div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid var(--border);">
@@ -667,16 +667,16 @@ class WebUIRenderer:
</div> </div>
<div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid var(--border);"> <div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 4px;">Latest Upstream Release</div> <div style="font-size: 12px; color: var(--text-muted); margin-bottom: 4px;">Latest Upstream Release</div>
<div style="font-size: 18px; font-weight: 700; color: #34d399;">v{update_status.get('latest_upstream_version') or LOGAR_VERSION}</div> <div id="latestUpstreamVersionText" style="font-size: 18px; font-weight: 700; color: #34d399;">v{update_status.get('latest_upstream_version') or LOGAR_VERSION}</div>
</div> </div>
<div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid var(--border);"> <div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 4px;">Last Checked</div> <div style="font-size: 12px; color: var(--text-muted); margin-bottom: 4px;">Last Checked</div>
<div style="font-size: 14px; font-weight: 600; color: #e2e8f0; margin-top: 3px;">{update_status.get('last_checked_utc') or 'Never'}</div> <div id="lastCheckedText" style="font-size: 14px; font-weight: 600; color: #e2e8f0; margin-top: 3px;">{update_status.get('last_checked_utc') or 'Never'}</div>
</div> </div>
</div> </div>
<div style="display: flex; gap: 12px; flex-wrap: wrap;"> <div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button type="button" onclick="checkForUpdatesNow()" class="btn btn-primary">🔍 Check for Upstream Updates</button> <button type="button" onclick="checkForUpdatesNow()" class="btn btn-primary">🔍 Check for Upstream Updates</button>
{(f'<a href="{html.escape(update_status.get("release_url", ""))}" target="_blank" class="btn btn-secondary">🌐 View Release on Gitea</a>' if update_status.get("release_url") else '')} <span id="viewReleaseLinkContainer">{(f'<a href="{html.escape(update_status.get("release_url", ""))}" target="_blank" class="btn btn-secondary">🌐 View Release on Gitea</a>' if update_status.get("release_url") else '')}</span>
</div> </div>
</div> </div>
@@ -771,18 +771,18 @@ class WebUIRenderer:
<tr> <tr>
<td><span class="badge badge-os">WINDOWS</span></td> <td><span class="badge badge-os">WINDOWS</span></td>
<td><strong>{html.escape(hosted_packages.get('windows', {}).get('filename') or 'LOGAR_Client_Setup.exe')}</strong></td> <td><strong>{html.escape(hosted_packages.get('windows', {}).get('filename') or 'LOGAR_Client_Setup.exe')}</strong></td>
<td><code>v{html.escape(str(hosted_packages.get('windows', {}).get('version') or hosted_client_ver))}</code></td> <td><code id="pkgWinVersion">v{html.escape(str(hosted_packages.get('windows', {}).get('version') or hosted_client_ver))}</code></td>
<td>{((hosted_packages.get('windows', {}).get('size_bytes', 0) // 1024) if hosted_packages.get('windows', {}).get('exists') else 0)} KB</td> <td><span id="pkgWinSize">{((hosted_packages.get('windows', {}).get('size_bytes', 0) // 1024) if hosted_packages.get('windows', {}).get('exists') else 0)} KB</span></td>
<td><code title="{html.escape(hosted_packages.get('windows', {}).get('sha256', ''))}">{(hosted_packages.get('windows', {}).get('sha256', 'N/A')[:16] + '...') if hosted_packages.get('windows', {}).get('sha256') else 'N/A'}</code></td> <td><code id="pkgWinSha" title="{html.escape(hosted_packages.get('windows', {}).get('sha256', ''))}">{(hosted_packages.get('windows', {}).get('sha256', 'N/A')[:16] + '...') if hosted_packages.get('windows', {}).get('sha256') else 'N/A'}</code></td>
<td>{('<span class="badge badge-active">Ready</span>' if hosted_packages.get('windows', {}).get('exists') else '<span class="badge badge-error">Failed</span>')}</td> <td><span id="pkgWinStatus">{('<span class="badge badge-active">Ready</span>' if hosted_packages.get('windows', {}).get('exists') else '<span class="badge badge-error">Failed</span>')}</span></td>
</tr> </tr>
<tr> <tr>
<td><span class="badge badge-os">LINUX</span></td> <td><span class="badge badge-os">LINUX</span></td>
<td><strong>{html.escape(hosted_packages.get('linux', {}).get('filename') or 'Linux_Client.bin')}</strong></td> <td><strong>{html.escape(hosted_packages.get('linux', {}).get('filename') or 'Linux_Client.bin')}</strong></td>
<td><code>v{html.escape(str(hosted_packages.get('linux', {}).get('version') or hosted_client_ver))}</code></td> <td><code id="pkgLinuxVersion">v{html.escape(str(hosted_packages.get('linux', {}).get('version') or hosted_client_ver))}</code></td>
<td>{((hosted_packages.get('linux', {}).get('size_bytes', 0) // 1024) if hosted_packages.get('linux', {}).get('exists') else 0)} KB</td> <td><span id="pkgLinuxSize">{((hosted_packages.get('linux', {}).get('size_bytes', 0) // 1024) if hosted_packages.get('linux', {}).get('exists') else 0)} KB</span></td>
<td><code title="{html.escape(hosted_packages.get('linux', {}).get('sha256', ''))}">{(hosted_packages.get('linux', {}).get('sha256', 'N/A')[:16] + '...') if hosted_packages.get('linux', {}).get('sha256') else 'N/A'}</code></td> <td><code id="pkgLinuxSha" title="{html.escape(hosted_packages.get('linux', {}).get('sha256', ''))}">{(hosted_packages.get('linux', {}).get('sha256', 'N/A')[:16] + '...') if hosted_packages.get('linux', {}).get('sha256') else 'N/A'}</code></td>
<td>{('<span class="badge badge-active">Ready</span>' if hosted_packages.get('linux', {}).get('exists') else '<span class="badge badge-error">Failed</span>')}</td> <td><span id="pkgLinuxStatus">{('<span class="badge badge-active">Ready</span>' if hosted_packages.get('linux', {}).get('exists') else '<span class="badge badge-error">Failed</span>')}</span></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -1440,9 +1440,19 @@ class WebUIRenderer:
if (btn && pane) {{ if (btn && pane) {{
btn.classList.add('active'); btn.classList.add('active');
pane.classList.add('active'); pane.classList.add('active');
try {{ localStorage.setItem('logar_active_tab', tabId); }} catch(e) {{}}
}} }}
}} }}
window.addEventListener('DOMContentLoaded', () => {{
try {{
const savedTab = localStorage.getItem('logar_active_tab');
if (savedTab && document.getElementById('tabBtn-' + savedTab)) {{
switchTab(savedTab);
}}
}} catch(e) {{}}
}});
function showAlert(msg, isSuccess = true) {{ function showAlert(msg, isSuccess = true) {{
const el = document.getElementById('statusAlert'); const el = document.getElementById('statusAlert');
el.className = 'alert ' + (isSuccess ? 'alert-success' : 'alert-error'); el.className = 'alert ' + (isSuccess ? 'alert-success' : 'alert-error');
@@ -1935,17 +1945,71 @@ class WebUIRenderer:
el.appendChild(img); el.appendChild(img);
}} }}
function updatePackageTableInPlace(pkgs) {{
if (!pkgs) return;
if (pkgs.windows) {{
const w = pkgs.windows;
const vEl = document.getElementById('pkgWinVersion');
if (vEl && w.version) vEl.textContent = 'v' + w.version;
const szEl = document.getElementById('pkgWinSize');
if (szEl && w.size_bytes !== undefined) szEl.textContent = Math.floor((w.size_bytes || 0) / 1024) + ' KB';
const shaEl = document.getElementById('pkgWinSha');
if (shaEl && w.sha256) {{ shaEl.textContent = w.sha256.substring(0, 16) + '...'; shaEl.title = w.sha256; }}
const stEl = document.getElementById('pkgWinStatus');
if (stEl) stEl.innerHTML = w.exists ? '<span class="badge badge-active">Ready</span>' : '<span class="badge badge-error">Failed</span>';
}}
if (pkgs.linux) {{
const l = pkgs.linux;
const vEl = document.getElementById('pkgLinuxVersion');
if (vEl && l.version) vEl.textContent = 'v' + l.version;
const szEl = document.getElementById('pkgLinuxSize');
if (szEl && l.size_bytes !== undefined) szEl.textContent = Math.floor((l.size_bytes || 0) / 1024) + ' KB';
const shaEl = document.getElementById('pkgLinuxSha');
if (shaEl && l.sha256) {{ shaEl.textContent = l.sha256.substring(0, 16) + '...'; shaEl.title = l.sha256; }}
const stEl = document.getElementById('pkgLinuxStatus');
if (stEl) stEl.innerHTML = l.exists ? '<span class="badge badge-active">Ready</span>' : '<span class="badge badge-error">Failed</span>';
}}
}}
async function checkForUpdatesNow() {{ async function checkForUpdatesNow() {{
showAlert('Checking upstream repository for new releases...'); showAlert('Checking upstream repository for new releases...');
try {{ try {{
const res = await fetch('/api/server/update/check', {{ method: 'POST' }}); const res = await fetch('/api/server/update/check', {{ method: 'POST' }});
const data = await res.json(); const data = await res.json();
const badgeEl = document.getElementById('upstreamReleaseStatusBadge');
if (badgeEl) {{
badgeEl.innerHTML = data.server_update_available
? '<span class="badge badge-warning">UPDATE AVAILABLE</span>'
: '<span class="badge badge-active">UP TO DATE</span>';
}}
const verEl = document.getElementById('latestUpstreamVersionText');
if (verEl && data.latest_upstream_version) {{
verEl.textContent = 'v' + data.latest_upstream_version;
}}
const checkEl = document.getElementById('lastCheckedText');
if (checkEl) {{
checkEl.textContent = data.last_checked_utc || (new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC');
}}
const linkEl = document.getElementById('viewReleaseLinkContainer');
if (linkEl) {{
linkEl.innerHTML = data.release_url
? '<a href="' + data.release_url + '" target="_blank" class="btn btn-secondary">🌐 View Release on Gitea</a>'
: '';
}}
if (data.hosted_packages) {{
updatePackageTableInPlace(data.hosted_packages);
}}
if (data.server_update_available) {{ if (data.server_update_available) {{
showAlert('🔔 Update Available: v' + data.latest_upstream_version + ' is ready!'); showAlert('🔔 Update Available: v' + data.latest_upstream_version + ' is ready!');
}} else {{ }} else {{
showAlert('✅ LOGAR is up to date (Current: v' + data.current_server_version + ').'); showAlert('✅ LOGAR is up to date (Current: v' + (data.current_server_version || '0.3.5.2') + ').');
}} }}
setTimeout(() => location.reload(), 1200);
}} catch (err) {{ }} catch (err) {{
alert('Failed to check for updates: ' + err.message); alert('Failed to check for updates: ' + err.message);
}} }}
@@ -1972,7 +2036,6 @@ class WebUIRenderer:
const data = await res.json(); const data = await res.json();
if (res.ok && data.status === 'success') {{ if (res.ok && data.status === 'success') {{
showAlert('✅ Fleet update configuration saved successfully!'); showAlert('✅ Fleet update configuration saved successfully!');
setTimeout(() => location.reload(), 1200);
}} else {{ }} else {{
let errMsg = 'Failed to save configuration'; let errMsg = 'Failed to save configuration';
if (typeof data.detail === 'string') errMsg = data.detail; if (typeof data.detail === 'string') errMsg = data.detail;
@@ -1992,7 +2055,14 @@ class WebUIRenderer:
if (res.ok && data.status === 'success') {{ if (res.ok && data.status === 'success') {{
const dl = (data.results && data.results.downloaded) ? data.results.downloaded.length : 0; const dl = (data.results && data.results.downloaded) ? data.results.downloaded.length : 0;
showAlert('✅ Upstream synchronization complete (' + dl + ' package(s) downloaded/verified)!'); showAlert('✅ Upstream synchronization complete (' + dl + ' package(s) downloaded/verified)!');
setTimeout(() => location.reload(), 1500);
try {{
const statusRes = await fetch('/api/server/update/status');
const statusData = await statusRes.json();
if (statusData && statusData.hosted_packages) {{
updatePackageTableInPlace(statusData.hosted_packages);
}}
}} catch(e) {{}}
}} else {{ }} else {{
let errMsg = 'Synchronization failed'; let errMsg = 'Synchronization failed';
if (typeof data.detail === 'string') errMsg = data.detail; if (typeof data.detail === 'string') errMsg = data.detail;