diff --git a/containers/libreportal/frontend/components/apps/core/js/app-tabbed-manager.js b/containers/libreportal/frontend/components/apps/core/js/app-tabbed-manager.js index 34de23e..c057687 100755 --- a/containers/libreportal/frontend/components/apps/core/js/app-tabbed-manager.js +++ b/containers/libreportal/frontend/components/apps/core/js/app-tabbed-manager.js @@ -589,27 +589,36 @@ class AppTabbedManager { section.innerHTML = '
Loading…
'; await this.appUpdater.refreshAll(); const app = (this.appUpdater.apps || []).find((x) => x.name === this.currentApp); - if (!app) { - section.innerHTML = `
No update data for this app yet.
`; - return; - } - section.innerHTML = this.renderAppUpdaterHead(app) + this.appUpdater.renderAppDetail(app); + // Always lead with the title block + recessed dark container, so the tab + // matches the Config / Backups / Tasks tabs whether or not there's data. + const body = app + ? this.appUpdater.renderAppDetail(app, { includeVersion: true }) + : `
No update data for this app yet.
`; + section.innerHTML = this.renderAppUpdaterHead(app) + + `
${body}
`; } + // Title block for the per-app Updates tab — emoji + title + description on the + // left, Check/Update actions on the right. Mirrors .backup-title (.config-title + // family) so every app-detail tab shares one header idiom. `a` may be null + // (no data yet); the Update button only appears when an update is available. 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 - ? `update available` - : (a.scanned ? `up to date` : `unscanned`); - const updBtn = a.update_available + const esc = (s) => (this.appUpdater ? this.appUpdater.escape(s) : String(s == null ? '' : s)); + const name = this.currentApp + ? (window.getAppDisplayName ? window.getAppDisplayName(this.currentApp) : this.currentApp) + : 'this app'; + const updBtn = (a && a.update_available) ? `` : ''; - return `
-
${badge} ${cur}${avail ? ` ${avail}` : ''}
-
${updBtn}
+ return `
+
+

⬆️ Updates

+

Version, security and recovery for ${esc(name)}.

+
+
+ + ${updBtn} +
`; } diff --git a/containers/libreportal/frontend/components/apps/overview/css/overview.css b/containers/libreportal/frontend/components/apps/overview/css/overview.css index 0a2c6d2..a42733d 100644 --- a/containers/libreportal/frontend/components/apps/overview/css/overview.css +++ b/containers/libreportal/frontend/components/apps/overview/css/overview.css @@ -199,19 +199,53 @@ #overview-view .ov-subtabs-content .tab-panel { display: none; } #overview-view .ov-subtabs-content .tab-panel.active { display: block; } -/* ---- per-app Updates tab header ----------------------------------------- */ -.app-updater-section { padding: 4px 0; } -.app-updater-head { +/* ---- per-app Updates tab (app-detail) ----------------------------------- */ +/* Same idiom as the Config / Backups / Tasks tabs: a title block (icon + h3 + + description, with a bottom-border divider) followed by a recessed dark + container holding the body. Mirrors .backup-section / .backup-title / + .backup-snapshots-container. */ +.app-updater-section { 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; + flex-direction: column; + padding: 0; +} +.updater-title { + padding: 20px; + background: transparent; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} +.updater-title-main { flex: 1; min-width: 0; } +.updater-title h3 { + margin: 0 0 8px 0; + color: var(--text-primary, #fff); + font-size: 18px; + font-weight: 600; +} +.updater-title p { + margin: 0; + color: var(--text-secondary, #ccc); + font-size: 13px; +} +.updater-title-actions { display: flex; gap: 10px; align-items: center; flex-shrink: 0; } + +/* Recessed dark panel wrapping the body — same recipe as .backup-snapshots- + container / .tasks-container. */ +.updater-detail-container { + padding: 4px 16px; + margin: 16px; + background: rgba(var(--bg-rgb), 0.2); + border-radius: 8px; +} +/* Inside the container the Version / Security / Recovery / History sections are + separated by divider lines (the fleet row-details, not in this container, keep + their gap-only spacing). */ +.updater-detail-container .updater-detail { gap: 0; padding-top: 0; } +.updater-detail-container .updater-detail-section { padding: 16px 0; } +.updater-detail-container .updater-detail-section:first-child { padding-top: 0; } +.updater-detail-container .updater-detail-section + .updater-detail-section { + border-top: 1px solid var(--border-color); } -.app-updater-head-main { display: flex; align-items: center; gap: 10px; } -.app-updater-head-actions { display: flex; gap: 8px; } diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js index e5583c3..eb1e774 100644 --- a/containers/libreportal/frontend/components/updater/js/updater-page.js +++ b/containers/libreportal/frontend/components/updater/js/updater-page.js @@ -386,8 +386,21 @@ class UpdaterPage { // verbatim as the fleet Updates expander body (overview-manager.js) AND the // per-app Updater tab. Action buttons keep the data-updater-action/data-app // contract, so whichever delegated handler is in scope drives them. - renderAppDetail(app) { + renderAppDetail(app, opts = {}) { const a = app || {}; + // Optional leading "Version" section — the per-app Updates tab shows the + // current/available version + status badge as a section in the panel. The + // fleet rows omit it (the row head already shows the version). + let versionSection = ''; + if (opts.includeVersion) { + const cur = this.escape(a.current_version || a.current_image || '—'); + const avail = a.update_available ? this.escape(a.available_version || a.available_image || 'newer') : null; + const badge = a.update_available + ? `update available` + : (a.scanned ? `up to date` : `unscanned`); + versionSection = `

Version

+
${badge} ${cur}${avail ? ` ${avail}` : ''}
`; + } const cves = a.cves || []; const cveItems = cves.map((c) => `
@@ -423,7 +436,7 @@ class UpdaterPage { ${this.escape(e.from || '')}${e.to ? ` → ${this.escape(e.to)}` : ''} ${this.fmtRel(e.ts)}
`).join('')}
` : ''; - return `
${security}${recovery}${history}
`; + return `
${versionSection}${security}${recovery}${history}
`; } empty(msg, withCheck) {