Fleet console: triage band, activity feed, live state first

This commit is contained in:
2026-08-28 22:35:16 -04:00
parent 2a94c3c2d2
commit 7e779a6829
6 changed files with 448 additions and 28 deletions

View File

@@ -49,12 +49,32 @@
<span class="hero-detail-value">{{ computer.locationname }}</span>
</div>
</div>
<!-- The 6am band: is it alive, what stops without it, how do I reach
it. "Serves machine" used to sit in the relationships card near
the bottom of the page, which is the wrong place for the single
most useful fact about a shop-floor PC. -->
<AssetTriageBand
:assetid="computer.assetid"
:liveness="liveness"
:accessmethods="computer.accessmethods || []"
/>
</div>
</div>
<!-- Canonical card order: Identity -> type-specific -> status -> Location & Organization -> domain -> Custom Fields -> Warranty -> Relationships -> Notes -> audit footer -->
<!-- Main Content Grid -->
<!-- Live state before reference data. The grid is a two-column FLOW
(column-count, with display:contents on the column wrappers), so a
card cannot be pinned to a side - only document order decides where
it lands. Putting the contributed panels first means endpoint status,
services and activity are what a responder meets, and Identity,
Hardware and the rest follow. Panels self-order among themselves by
their declared position, and self-hide when a site has no plugin
answering for this asset type. -->
<div class="content-grid">
<PluginAssetPanels ref="assetPanels" :assetid="computer.assetid" />
<!-- Left Column -->
<div class="content-column">
<!-- Identity Section -->
@@ -125,20 +145,19 @@
</div>
</div>
<!-- Remote Access -->
<div class="section-card">
<h3 class="section-title">Remote Access</h3>
<div class="access-methods">
<template v-for="a in (computer.accessmethods || [])" :key="a.id">
<a v-if="a.link" :href="a.link" class="access-link" :title="a.link">{{ a.name }}</a>
<span v-else class="access-link disabled" title="No hostname/IP set for this PC">{{ a.name }}</span>
</template>
<span v-if="!(computer.accessmethods || []).length" class="muted">None configured</span>
</div>
</div>
<!-- Remote Access lives in the triage band at the top of the page
now: it is what a responder clicks first, and rendering it twice
made the page longer without telling anyone anything. -->
<!-- Status / Check-in -->
<div class="section-card">
<!-- Status / Check-in, from the COMPUTER RECORD (the asset-report
collector), which is a different source from the endpoints
agent's live telemetry panel. Shown only when that record holds
something: on a PC with an agent these fields sit empty while
the endpoint panel is fully populated, so an always-on card
reported "Last Reported: Never" next to live telemetry saying
otherwise. Two sources disagreeing in public is worse than one
source missing. -->
<div v-if="hasRecordCheckin" class="section-card">
<h3 class="section-title">Status</h3>
<div class="info-list">
<div class="info-row">
@@ -249,9 +268,6 @@
<!-- Custom Fields -->
<CustomFieldsSection :assetid="computer.assetid" />
<!-- Warranty -->
<PluginAssetPanels ref="assetPanels" :assetid="computer.assetid" />
<!-- All relationships (controls, defaultprinter, ...) -->
<AssetRelationships v-if="computer.assetid" :assetid="computer.assetid" />
@@ -285,6 +301,7 @@ import { withBase } from '@/utils/basePath'
import { useWarrantyBadge } from '@/composables/warrantyBadge'
import LocationMapTooltip from '@/components/LocationMapTooltip.vue'
import CustomFieldsSection from '@/components/CustomFieldsSection.vue'
import AssetTriageBand from '@/components/AssetTriageBand.vue'
import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
import AssetRelationships from '@/components/AssetRelationships.vue'
import { useIdentifierFlags } from '@/composables/identifierSettings'
@@ -301,6 +318,18 @@ const { isEnabled } = useIdentifierFlags()
const loading = ref(true)
const computer = ref(null)
// Liveness for the triage band. Null when the endpoints plugin is absent, or
// when the viewer lacks endpoints.view - in either case the chip is simply not
// shown, which is better than rendering "unknown" and implying a fault.
const liveness = ref(null)
// The computer record's own check-in fields, as opposed to live agent
// telemetry. Empty on an agent-managed PC, populated where the asset-report
// script runs instead.
const hasRecordCheckin = computed(() => {
const record = computer.value?.computer || {}
return Boolean(record.loggedinuser || record.lastreporteddate || record.lastboottime)
})
const installedApps = ref([])
const assetPanels = ref(null)
const restarting = ref(false)
@@ -317,6 +346,42 @@ const { heroWarranty, warrantyDate: formatWarrantyDate } = useWarrantyBadge(() =
// relationships render via the shared AssetRelationships card
async function loadLiveness(assetid) {
if (!assetid) return
try {
const response = await endpointsApi.get(assetid)
const state = response.data.data || {}
// The SERVER decides reporting/quiet/never, because the quiet window is a
// site setting. Guessing a threshold here would disagree with the Fleet
// list about the same endpoint.
const labels = {
reporting: `Reporting, ${relativeAge(state.lastreportedat)}`,
quiet: `Quiet, last seen ${relativeAge(state.lastreportedat)}`,
never: 'Never reported'
}
const levels = { reporting: 'ok', quiet: 'warning', never: 'unknown' }
if (state.status) {
liveness.value = { level: levels[state.status], label: labels[state.status] }
}
} catch (error) {
// 404 = no agent on this PC, 403 = the viewer cannot read telemetry.
// Neither is a page error; the chip just does not appear.
liveness.value = null
}
}
function relativeAge(value) {
if (!value) return 'never'
const then = new Date(value)
if (isNaN(then)) return 'unknown'
const mins = Math.floor((Date.now() - then.getTime()) / 60000)
if (mins < 1) return 'just now'
if (mins < 60) return `${mins} m ago`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours} h ago`
return `${Math.floor(hours / 24)} d ago`
}
onMounted(async () => {
try {
const response = await computersApi.get(route.params.id)
@@ -329,6 +394,7 @@ onMounted(async () => {
installedApps.value = appsResponse.data.data || []
} catch (appError) {
}
loadLiveness(computer.value?.assetid)
// Warranties load via useWarrantyBadge (watches computer.assetid).
} catch (error) {
console.error('Error loading computer:', error)