diff --git a/src/api/admin/i18n.rs b/src/api/admin/i18n.rs index 4a340aa..ca7f4e5 100644 --- a/src/api/admin/i18n.rs +++ b/src/api/admin/i18n.rs @@ -999,6 +999,35 @@ pub fn t(lang: Lang, key: &str) -> &'static str { "SSID-uri din apropiere ({0})", "SSID cercanos ({0})", ), + "devices.installed_software" => ( + "Installed software", + "Installierte Software", + "Logiciels installés", + "Software instalat", + "Software instalado", + ), + "devices.installed_software_count" => ( + "Show {0} entries", + "{0} Einträge anzeigen", + "Afficher {0} entrées", + "Afișați {0} intrări", + "Mostrar {0} entradas", + ), + "devices.col_software_name" => ("Name", "Name", "Nom", "Nume", "Nombre"), + "devices.col_software_version" => ( + "Version", + "Version", + "Version", + "Versiune", + "Versión", + ), + "devices.col_software_publisher" => ( + "Publisher", + "Herausgeber", + "Éditeur", + "Editor", + "Editor", + ), "devices.public_ip" => ( "Public IP (egress, last lookup)", "Öffentliche IP (Egress, letzte Abfrage)", diff --git a/src/api/admin/pages/devices.rs b/src/api/admin/pages/devices.rs index f195eaa..e21e8db 100644 --- a/src/api/admin/pages/devices.rs +++ b/src/api/admin/pages/devices.rs @@ -618,6 +618,7 @@ fn render_inventory_table(lang: Lang, inv: &serde_json::Value) -> String { let nics_html = render_nics(lang, inv.get("network_interfaces")); let wifi_html = render_wifi(lang, inv.get("wifi_current"), inv.get("wifi_nearby")); + let software_html = render_installed_software(lang, inv.get("installed_software")); let public_ip_raw = inv .get("public_ip") .and_then(|v| v.as_str()) @@ -668,6 +669,7 @@ fn render_inventory_table(lang: Lang, inv: &serde_json::Value) -> String {

{l_pip}

{public_ip} + {software}

{l_bl}

{bl} @@ -688,6 +690,7 @@ fn render_inventory_table(lang: Lang, inv: &serde_json::Value) -> String { nics = nics_html, wifi = wifi_html, public_ip = public_ip_html, + software = software_html, l_disks = t(lang, "devices.disks"), l_nics = t(lang, "devices.network_interfaces"), l_pip = t(lang, "devices.public_ip"), @@ -871,6 +874,75 @@ fn render_wifi( ) } +/// Render the `installed_software` array as a collapsed `
` table. +/// +/// hello-agent enumerates the Windows Add/Remove Programs registry hives +/// and uploads the result under `inventory.installed_software` as a sorted +/// array of `{name, version, publisher, install_date, bitness}` objects. +/// We render it collapsed by default because the list is routinely +/// 100-300 entries on a real machine — expanding it inline would push +/// the rest of the inventory off the screen. The summary line carries +/// the entry count so the operator can see "is there anything here" +/// without opening it. +/// +/// Returns an empty string when the field is absent or empty so the +/// surrounding template can drop the entire block — agents that don't +/// (or can't) report installed software don't get a stray empty header. +fn render_installed_software(lang: Lang, sw: Option<&serde_json::Value>) -> String { + let arr = match sw { + Some(serde_json::Value::Array(a)) if !a.is_empty() => a, + _ => return String::new(), + }; + + let mut rows = String::new(); + for entry in arr { + let name = fmt_inv_value(entry.get("name")); + let version = fmt_inv_value(entry.get("version")); + let publisher = fmt_inv_value(entry.get("publisher")); + // Bitness is a small badge next to the name when present, matching + // the Wi-Fi badge on the NICs table. We treat the absence of the + // field as "unknown" and render nothing rather than a "?" — the + // page doesn't owe the operator a verdict it can't actually make. + let bitness_raw = entry + .get("bitness") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let bitness_badge = match bitness_raw { + "64" => { + r##" x64"## + } + "32" => { + r##" x86"## + } + _ => "", + }; + let _ = write!( + &mut rows, + r##"{name}{badge}{version}{publisher}"##, + name = name, + badge = bitness_badge, + version = version, + publisher = publisher, + ); + } + + format!( + r##"
+

{label}

+
+ {count_label} +
{rows}
{c_name}{c_version}{c_publisher}
+
+
"##, + label = t(lang, "devices.installed_software"), + count_label = tf1(lang, "devices.installed_software_count", &arr.len().to_string()), + c_name = t(lang, "devices.col_software_name"), + c_version = t(lang, "devices.col_software_version"), + c_publisher = t(lang, "devices.col_software_publisher"), + rows = rows, + ) +} + /// Render an elapsed-seconds count as a short "Xs / Xm / Xh / Xd" string /// for the offline tooltip. The exact heartbeat timestamp is already /// shown in the table cell — this is just for the friendly tooltip.