Compare commits

...

2 Commits

Author SHA1 Message Date
librelad
18ff440115 Merge claude/1 2026-05-31 23:54:21 +01:00
librelad
1460acb941 feat(webui): add per-app Updates tab (version/CVEs/recovery/history)
New 'Updates' tab in the app detail page, beside Backups. Reuses the headless
UpdaterPage + renderAppDetail() scoped to the single app, so the per-app and
fleet views share one data/render path. UpdaterPage is added to the apps script
bundle so it's available on app pages; the tab is disabled while a task runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 23:54:21 +01:00
4 changed files with 91 additions and 2 deletions

View File

@ -119,6 +119,10 @@
<button class="main-tab-button" data-tab="backups" onclick="if(window.appTabbedManager) window.appTabbedManager.switchTab('backups')"> <button class="main-tab-button" data-tab="backups" onclick="if(window.appTabbedManager) window.appTabbedManager.switchTab('backups')">
<span class="tab-emoji">💾</span> <span class="tab-emoji">💾</span>
<span class="tab-name">Backups</span> <span class="tab-name">Backups</span>
</button>
<button class="main-tab-button" data-tab="updater" onclick="if(window.appTabbedManager) window.appTabbedManager.switchTab('updater')">
<span class="tab-emoji">⬆️</span>
<span class="tab-name">Updates</span>
</button> </button>
<button class="main-tab-button" data-tab="tasks" onclick="if(window.appTabbedManager) window.appTabbedManager.switchTab('tasks')"> <button class="main-tab-button" data-tab="tasks" onclick="if(window.appTabbedManager) window.appTabbedManager.switchTab('tasks')">
<span class="tab-emoji">📋</span> <span class="tab-emoji">📋</span>
@ -222,6 +226,13 @@
</div> </div>
</div> </div>
<!-- Updates Tab (per-app: version state, CVEs, recovery, history) -->
<div class="tab-pane" id="updater-tab">
<div class="app-updater-section" id="app-updater-section">
<div class="updater-empty">Loading…</div>
</div>
</div>
<!-- Tasks Tab --> <!-- Tasks Tab -->
<div class="tab-pane" id="tasks-tab"> <div class="tab-pane" id="tasks-tab">
<div class="tasks-section"> <div class="tasks-section">

View File

@ -292,6 +292,10 @@ class AppTabbedManager {
// IMPORTANT: Re-apply button state if there are running tasks // IMPORTANT: Re-apply button state if there are running tasks
this.restoreButtonState(); this.restoreButtonState();
break; break;
case 'updater':
await this.loadAppUpdater();
this.restoreButtonState();
break;
case 'services': case 'services':
if (window.servicesManager) { if (window.servicesManager) {
await window.servicesManager.load(this.currentApp); await window.servicesManager.load(this.currentApp);
@ -553,6 +557,62 @@ class AppTabbedManager {
await this.backupAppCard.render(); await this.backupAppCard.render();
} }
// Load the per-app Updates tab — version state + CVEs + recovery + history for
// the current app. Reuses the headless UpdaterPage: the same data layer the
// fleet Overview uses, and renderAppDetail() for the body. Actions dispatch
// through the task system, identical to the fleet view.
async loadAppUpdater() {
const section = document.getElementById('app-updater-section');
if (!section) return;
if (typeof UpdaterPage === 'undefined') {
section.innerHTML = '<div class="updater-empty">Updater unavailable.</div>';
return;
}
if (!this.appUpdater) this.appUpdater = new UpdaterPage((window.LP && window.LP.services) || {});
// One delegated listener scoped to this section (the pane lives in the
// shared layout, replaced on full navigation, so it GCs with the node).
if (section.dataset.updBound !== '1') {
section.dataset.updBound = '1';
section.addEventListener('click', (e) => {
const a = e.target.closest('[data-updater-action]');
if (!a) return;
const app = a.dataset.app || this.currentApp;
switch (a.dataset.updaterAction) {
case 'check': this.appUpdater.checkForUpdates(); break;
case 'update': this.appUpdater.applyUpdate(app); break;
case 'rollback': this.appUpdater.rollback(app); break;
}
});
}
section.innerHTML = '<div class="updater-empty">Loading…</div>';
await this.appUpdater.refreshAll();
const app = (this.appUpdater.apps || []).find((x) => x.name === this.currentApp);
if (!app) {
section.innerHTML = `<div class="updater-empty">No update data for this app yet. <button class="updater-btn updater-btn-primary" data-updater-action="check">Check now</button></div>`;
return;
}
section.innerHTML = this.renderAppUpdaterHead(app) + this.appUpdater.renderAppDetail(app);
}
renderAppUpdaterHead(a) {
const up = this.appUpdater;
const esc = (s) => up.escape(s);
const cur = esc(a.current_version || a.current_image || '—');
const avail = a.update_available ? esc(a.available_version || a.available_image || 'newer') : null;
const badge = a.update_available
? `<span class="updater-badge updater-badge-update">update available</span>`
: (a.scanned ? `<span class="updater-badge updater-badge-ok">up to date</span>` : `<span class="updater-badge updater-badge-unknown">unscanned</span>`);
const updBtn = a.update_available
? `<button class="updater-btn updater-btn-primary" data-updater-action="update" data-app="${esc(a.name)}">Update</button>`
: '';
return `<div class="app-updater-head">
<div class="app-updater-head-main">${badge} <span class="updater-row-ver">${cur}${avail ? ` <span class="updater-arrow">→</span> <strong>${avail}</strong>` : ''}</span></div>
<div class="app-updater-head-actions"><button class="updater-btn" data-updater-action="check"> Check</button> ${updBtn}</div>
</div>`;
}
// Initialize the tabbed manager // Initialize the tabbed manager
async initialize() { async initialize() {
// Prevent double initialization // Prevent double initialization
@ -849,7 +909,7 @@ class AppTabbedManager {
// Disable config, services and backup tabs when task is running // Disable config, services and backup tabs when task is running
disableTabs() { disableTabs() {
const tabs = ['config', 'services', 'tools', 'backups'] const tabs = ['config', 'services', 'tools', 'backups', 'updater']
.map(name => document.querySelector(`.main-tab-button[data-tab="${name}"], .tab-button[data-tab="${name}"]`)) .map(name => document.querySelector(`.main-tab-button[data-tab="${name}"], .tab-button[data-tab="${name}"]`))
.filter(Boolean); .filter(Boolean);
@ -864,7 +924,7 @@ class AppTabbedManager {
// Enable config, services and backup tabs when task completes // Enable config, services and backup tabs when task completes
enableTabs() { enableTabs() {
const tabs = ['config', 'services', 'tools', 'backups'] const tabs = ['config', 'services', 'tools', 'backups', 'updater']
.map(name => document.querySelector(`.main-tab-button[data-tab="${name}"], .tab-button[data-tab="${name}"]`)) .map(name => document.querySelector(`.main-tab-button[data-tab="${name}"], .tab-button[data-tab="${name}"]`))
.filter(Boolean); .filter(Boolean);

View File

@ -172,3 +172,20 @@
border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, .08)); border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, .08));
} }
.ov-loc-row span:first-child { font-weight: 500; } .ov-loc-row span:first-child { font-weight: 500; }
/* ---- per-app Updates tab header ----------------------------------------- */
.app-updater-section { padding: 4px 0; }
.app-updater-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
padding: 12px 14px;
border: 1px solid var(--border-color, rgba(255, 255, 255, .12));
border-radius: 10px;
background: var(--input-bg, rgba(255, 255, 255, .03));
margin-bottom: 4px;
}
.app-updater-head-main { display: flex; align-items: center; gap: 10px; }
.app-updater-head-actions { display: flex; gap: 8px; }

View File

@ -195,6 +195,7 @@ class SystemLoader {
'/components/apps/port-manager/js/port-manager.js', '/components/apps/port-manager/js/port-manager.js',
'/core/tasks/js/task-manager.js', // Add TaskManager for backup functionality '/core/tasks/js/task-manager.js', // Add TaskManager for backup functionality
'/core/backup-card/js/backup-app-card.js', '/core/backup-card/js/backup-app-card.js',
'/components/updater/js/updater-page.js', // headless reuse: per-app Updates tab + fleet Overview
'/components/apps/services/js/services-manager.js', '/components/apps/services/js/services-manager.js',
'/components/apps/tools/js/tools-manager.js', '/components/apps/tools/js/tools-manager.js',
'/components/apps/routing/js/routing-manager.js', '/components/apps/routing/js/routing-manager.js',