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
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m43s
This commit is contained in:
+86
-16
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user