@@ -285,6 +294,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 +311,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 +339,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 +387,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)
diff --git a/plugins/endpoints/api/routes.py b/plugins/endpoints/api/routes.py
index 68c8b69a..7f138402 100644
--- a/plugins/endpoints/api/routes.py
+++ b/plugins/endpoints/api/routes.py
@@ -306,7 +306,13 @@ def _health_for(state, criticalcount):
@endpoints_bp.route('/', methods=['GET'])
@require_permission('endpoints.view')
def get_endpoint(asset_id: int):
- """Get the current state for one endpoint by asset id."""
+ """Get the current state for one endpoint by asset id.
+
+ Carries the same reporting/quiet/never verdict the fleet list uses. The
+ quiet window is a site setting, so the SERVER decides it: a caller that
+ guessed a threshold in the browser would disagree with the fleet list on
+ the same endpoint the moment a site changed the setting.
+ """
state = EndpointState.query.filter_by(assetid=asset_id).first()
if not state:
@@ -316,7 +322,18 @@ def get_endpoint(asset_id: int):
http_code=404,
)
- return success_response(state.to_dict())
+ cutoff = _naive_utcnow() - timedelta(hours=_quiet_hours())
+ if state.lastreportedat is None:
+ status = 'never'
+ elif state.lastreportedat < cutoff:
+ status = 'quiet'
+ else:
+ status = 'reporting'
+
+ data = state.to_dict()
+ data['status'] = status
+ data['uptimedisplay'] = _format_uptime(state.uptimeseconds)
+ return success_response(data)
@endpoints_bp.route('//events', methods=['GET'])