This commit is contained in:
@@ -999,6 +999,35 @@ pub fn t(lang: Lang, key: &str) -> &'static str {
|
|||||||
"SSID-uri din apropiere ({0})",
|
"SSID-uri din apropiere ({0})",
|
||||||
"SSID cercanos ({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" => (
|
"devices.public_ip" => (
|
||||||
"Public IP (egress, last lookup)",
|
"Public IP (egress, last lookup)",
|
||||||
"Öffentliche IP (Egress, letzte Abfrage)",
|
"Ö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 nics_html = render_nics(lang, inv.get("network_interfaces"));
|
||||||
let wifi_html = render_wifi(lang, inv.get("wifi_current"), inv.get("wifi_nearby"));
|
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
|
let public_ip_raw = inv
|
||||||
.get("public_ip")
|
.get("public_ip")
|
||||||
.and_then(|v| v.as_str())
|
.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>
|
<h4 class="text-xs uppercase text-slate-500 mb-1">{l_pip}</h4>
|
||||||
{public_ip}
|
{public_ip}
|
||||||
</div>
|
</div>
|
||||||
|
{software}
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-xs uppercase text-slate-500 mb-1">{l_bl}</h4>
|
<h4 class="text-xs uppercase text-slate-500 mb-1">{l_bl}</h4>
|
||||||
{bl}
|
{bl}
|
||||||
@@ -688,6 +690,7 @@ fn render_inventory_table(lang: Lang, inv: &serde_json::Value) -> String {
|
|||||||
nics = nics_html,
|
nics = nics_html,
|
||||||
wifi = wifi_html,
|
wifi = wifi_html,
|
||||||
public_ip = public_ip_html,
|
public_ip = public_ip_html,
|
||||||
|
software = software_html,
|
||||||
l_disks = t(lang, "devices.disks"),
|
l_disks = t(lang, "devices.disks"),
|
||||||
l_nics = t(lang, "devices.network_interfaces"),
|
l_nics = t(lang, "devices.network_interfaces"),
|
||||||
l_pip = t(lang, "devices.public_ip"),
|
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
|
/// Render an elapsed-seconds count as a short "Xs / Xm / Xh / Xd" string
|
||||||
/// for the offline tooltip. The exact heartbeat timestamp is already
|
/// for the offline tooltip. The exact heartbeat timestamp is already
|
||||||
/// shown in the table cell — this is just for the friendly tooltip.
|
/// shown in the table cell — this is just for the friendly tooltip.
|
||||||
|
|||||||
Reference in New Issue
Block a user