diff --git a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js
index 77a4531..c2e0c41 100644
--- a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js
+++ b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js
@@ -607,9 +607,21 @@ class OverviewManager {
const name = esc((window.getAppDisplayName ? window.getAppDisplayName(a.name) : null) || a.displayName || a.name);
const cur = esc(a.current_version || a.current_image || '—');
const avail = a.update_available ? esc(a.available_version || a.available_image || 'newer') : null;
+ // "✓ Up to date" is the whole truth only when nothing newer exists at all.
+ // Once a newer release line is published, a green all-clear sitting beside
+ // the "1.159.0 available" chip reads as a flat contradiction. The two ARE
+ // reconcilable — you are current on the line you track, and moving lines is
+ // a deliberate act — but a fleet row is a glance, not a place to reconcile
+ // anything. So drop the reassurance and let the chip carry the specifics.
+ // (The per-app detail keeps "up to date" on purpose: there it comes with a
+ // sentence explaining the distinction and an Upgrade button.)
const status = a.update_available
? `↑ Update available`
- : (a.scanned ? `✓ Up to date` : `• Unscanned`);
+ : (!a.scanned
+ ? `• Unscanned`
+ : (a.newer_version
+ ? `• Newer version`
+ : `✓ Up to date`));
const sev = a.worstSeverity ? `${a.worstSeverity}` : '';
// Automatic is the default, so only the apps that opt OUT carry a marker —
// 30-odd "auto" chips would be wallpaper, one "manual" chip is information.
diff --git a/scripts/webui/data/generators/updater/webui_updater_scan.sh b/scripts/webui/data/generators/updater/webui_updater_scan.sh
index ea3928d..e8c7662 100644
--- a/scripts/webui/data/generators/updater/webui_updater_scan.sh
+++ b/scripts/webui/data/generators/updater/webui_updater_scan.sh
@@ -290,6 +290,28 @@ updaterNewerVersionTag() {
# Human-readable version for display: OCI label → tag (versioned) → tag·shortdigest.
updaterDisplayVersion() {
local oci="$1" channel="$2" vtype="$3" digest="$4"
+ # The OCI version label is usually the app's own version and reads far
+ # better than a bare tag ("0.63.2" beats "latest"), which is why it wins.
+ # But that label is INHERITED from whichever base image the vendor built on
+ # unless they overwrite it, so it can describe the OS instead of the app:
+ # mongo:8.0 carries org.opencontainers.image.version=24.04 — its Ubuntu
+ # base — and the row then called MongoDB "24.04" and offered "24.04 → 8.0"
+ # as an upgrade, which is not a version transition at all.
+ #
+ # When we track a VERSIONED tag we already hold an authoritative statement
+ # of the app's version, so the label only wins if the two agree on their
+ # leading number. That keeps the richer label where it genuinely is the
+ # app's (nextcloud 34 → 34.0.1) and rejects it where it plainly is not
+ # (mongo 8.0 vs 24.04). Rolling tags are unaffected: there is no version in
+ # "latest" to check against, and the label is the whole point there.
+ if [ -n "$oci" ] && [ "$vtype" = "versioned" ] && [ -n "$channel" ]; then
+ local _ln _cn
+ _ln="$(printf '%s' "$oci" | grep -oE '[0-9]+' | head -1)"
+ _cn="$(printf '%s' "$channel" | grep -oE '[0-9]+' | head -1)"
+ if [ -n "$_ln" ] && [ -n "$_cn" ] && [ "$((10#$_ln))" != "$((10#$_cn))" ]; then
+ oci=""
+ fi
+ fi
if [ -n "$oci" ]; then printf '%s' "$oci"; return; fi
if [ "$vtype" = "versioned" ] && [ -n "$channel" ]; then printf '%s' "$channel"; return; fi
local short="${digest#sha256:}"; short="${short:0:7}"