feat: readable report and host detail layout in web UI (0.9.8)
Field grid for event/host cards, human-readable daily report labels, and normalized plain-text report bodies. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,30 +5,60 @@
|
||||
<template v-else-if="host">
|
||||
<h1>{{ host.display_name || host.hostname }}</h1>
|
||||
<div class="card report-meta">
|
||||
<p>
|
||||
<strong>Hostname:</strong> {{ host.hostname }}
|
||||
· <strong>ID:</strong> {{ host.id }}
|
||||
· <strong>Агент:</strong> <span :class="'agent-' + host.agent_status">{{ agentLabel(host.agent_status) }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Версия агента:</strong> {{ host.product_version || "—" }}
|
||||
· <strong>OS:</strong> {{ host.os_family }} {{ host.os_version || "" }}
|
||||
· <strong>IPv4:</strong> {{ host.ipv4 || "—" }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Heartbeat:</strong> {{ host.last_heartbeat_at ? formatDt(host.last_heartbeat_at) : "—" }}
|
||||
· <strong>Last seen:</strong> {{ formatDt(host.last_seen_at) }}
|
||||
· <strong>Инвентаризация:</strong>
|
||||
{{ host.last_inventory_at ? formatDt(host.last_inventory_at) : "—" }}
|
||||
· <strong>Событий:</strong> {{ host.event_count ?? 0 }}
|
||||
</p>
|
||||
<dl class="detail-fields">
|
||||
<div class="detail-field">
|
||||
<dt>Hostname</dt>
|
||||
<dd>{{ host.hostname }}</dd>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<dt>ID</dt>
|
||||
<dd>{{ host.id }}</dd>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<dt>Агент</dt>
|
||||
<dd><span :class="'agent-' + host.agent_status">{{ agentLabel(host.agent_status) }}</span></dd>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<dt>Версия агента</dt>
|
||||
<dd>{{ host.product_version || "—" }}</dd>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<dt>OS</dt>
|
||||
<dd>{{ host.os_family }} {{ host.os_version || "" }}</dd>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<dt>IPv4</dt>
|
||||
<dd>{{ host.ipv4 || "—" }}</dd>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<dt>Heartbeat</dt>
|
||||
<dd>{{ host.last_heartbeat_at ? formatDt(host.last_heartbeat_at) : "—" }}</dd>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<dt>Last seen</dt>
|
||||
<dd>{{ formatDt(host.last_seen_at) }}</dd>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<dt>Инвентаризация</dt>
|
||||
<dd>{{ host.last_inventory_at ? formatDt(host.last_inventory_at) : "—" }}</dd>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<dt>Событий</dt>
|
||||
<dd>{{ host.event_count ?? 0 }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<section v-if="inventory" class="host-inventory card">
|
||||
<h2>Железо и ПО</h2>
|
||||
<div v-if="windowsInfo" class="inv-block">
|
||||
<h3>Windows</h3>
|
||||
<p>{{ windowsInfo }}</p>
|
||||
<dl class="detail-fields detail-fields-compact">
|
||||
<div v-for="item in windowsFields" :key="item.label" class="detail-field">
|
||||
<dt>{{ item.label }}</dt>
|
||||
<dd>{{ item.value }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div v-if="computerName" class="inv-block">
|
||||
<p><strong>Имя компьютера:</strong> {{ computerName }}</p>
|
||||
@@ -38,7 +68,7 @@
|
||||
<ul>
|
||||
<li v-for="(p, i) in processors" :key="'cpu-' + i">
|
||||
{{ p.name }}
|
||||
<span v-if="p.cores != null"> — {{ p.cores }} ядер / {{ p.logical_processors }} потоков</span>
|
||||
<span v-if="p.cores != null"> — {{ p.cores }} ядер / {{ p.logical }} потоков</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -160,13 +190,21 @@ const hostId = computed(() => parseInt(props.id, 10));
|
||||
|
||||
const inventory = computed(() => host.value?.inventory ?? null);
|
||||
|
||||
const windowsInfo = computed(() => {
|
||||
const windowsFields = computed(() => {
|
||||
const w = inventory.value?.windows as Record<string, string> | undefined;
|
||||
if (!w) return "";
|
||||
const parts = [w.product_name, w.version, w.os_hardware_abstraction_layer].filter(Boolean);
|
||||
return parts.join(" · ");
|
||||
if (!w) return [];
|
||||
const labels: Record<string, string> = {
|
||||
product_name: "Продукт",
|
||||
version: "Версия",
|
||||
os_hardware_abstraction_layer: "HAL",
|
||||
};
|
||||
return Object.entries(labels)
|
||||
.map(([key, label]) => ({ label, value: w[key]?.trim() || "" }))
|
||||
.filter((item) => item.value);
|
||||
});
|
||||
|
||||
const windowsInfo = computed(() => windowsFields.value.length > 0);
|
||||
|
||||
const computerName = computed(() => {
|
||||
const v = inventory.value?.computer_name;
|
||||
return typeof v === "string" && v.trim() ? v : "";
|
||||
@@ -175,7 +213,13 @@ const computerName = computed(() => {
|
||||
const processors = computed(() => {
|
||||
const raw = inventory.value?.processor;
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.filter((p): p is Record<string, unknown> => p != null && typeof p === "object");
|
||||
return raw
|
||||
.filter((p): p is Record<string, unknown> => p != null && typeof p === "object")
|
||||
.map((p) => ({
|
||||
name: String(p.name ?? "—"),
|
||||
cores: typeof p.cores === "number" ? p.cores : null,
|
||||
logical: typeof p.logical_processors === "number" ? p.logical_processors : null,
|
||||
}));
|
||||
});
|
||||
|
||||
const motherboard = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user