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 = '
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) +
+ `
-
${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 `