This commit is contained in:
@@ -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)",
|
||||
|
||||
@@ -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 {
|
||||
<h4 class="text-xs uppercase text-slate-500 mb-1">{l_pip}</h4>
|
||||
{public_ip}
|
||||
</div>
|
||||
{software}
|
||||
<div>
|
||||
<h4 class="text-xs uppercase text-slate-500 mb-1">{l_bl}</h4>
|
||||
{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 `<details>` 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##" <span class="ml-1 inline-block text-[10px] px-1 py-0 rounded bg-slate-800 text-slate-400">x64</span>"##
|
||||
}
|
||||
"32" => {
|
||||
r##" <span class="ml-1 inline-block text-[10px] px-1 py-0 rounded bg-slate-800 text-slate-500">x86</span>"##
|
||||
}
|
||||
_ => "",
|
||||
};
|
||||
let _ = write!(
|
||||
&mut rows,
|
||||
r##"<tr class="border-t border-slate-800 align-top"><td class="px-2 py-1 text-slate-200">{name}{badge}</td><td class="px-2 py-1 font-mono text-slate-300">{version}</td><td class="px-2 py-1 text-slate-400">{publisher}</td></tr>"##,
|
||||
name = name,
|
||||
badge = bitness_badge,
|
||||
version = version,
|
||||
publisher = publisher,
|
||||
);
|
||||
}
|
||||
|
||||
format!(
|
||||
r##"<div>
|
||||
<h4 class="text-xs uppercase text-slate-500 mb-1">{label}</h4>
|
||||
<details class="rounded-md border border-slate-800 bg-slate-900">
|
||||
<summary class="cursor-pointer px-3 py-2 text-xs text-slate-400 hover:text-slate-200 select-none">{count_label}</summary>
|
||||
<div class="p-2 overflow-x-auto"><table class="w-full text-xs"><thead><tr class="text-slate-500"><th class="text-left font-medium px-2 py-1">{c_name}</th><th class="text-left font-medium px-2 py-1">{c_version}</th><th class="text-left font-medium px-2 py-1">{c_publisher}</th></tr></thead><tbody>{rows}</tbody></table></div>
|
||||
</details>
|
||||
</div>"##,
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user