Rebuild the dashboard around what needs a person
The board was one card per source in a fixed grid, and both halves of that hurt. Every source got the same small box whatever it had to say. Warranties showed 5 of 50 behind "and 45 more"; the crash card truncated at 4; a one-row card sat beside a four-row card and left a hole. Nothing had room, which is the complaint that started this: not enough space to relay the data. And a box per source flattened priority. An enforcement failure and a warranty notice looked equally urgent because each owned a card, while the largest type on the page belonged to counts that are true every day and need nobody. WHAT THE PAGE IS NOW A severity rollup, one worst-first queue of everything that needs a person, and a row of context beneath it. The queue merges every critical and warning row and sorts them, so reading order is work order. It is full width, which is where the room came from. Rows are capped per source, because fifty expiring warranties must not bury two crashed PCs, and what did not fit is named per source with a link to it - a bare "and 45 more" that leads nowhere is worse than saying nothing. The rollup counts ROWS, not cards, and filters the board in place rather than navigating away, so the counts and the rows cannot disagree. Severity, position and permission were already declared per card and already sorted on. The old layout threw that away; this only stops it. WHAT WENT, AND WHY IT SHOULD NOT COME BACK Four asset tiles plus an "All assets" tile: machines + PCs + printers + network IS all assets, so the page showed a total beside its own addends at the same weight. It is now a line under a rule, where a total belongs. The printer strip. It carried the printer count a third time - the tile row and "Total Printers" were already the same number - and "Online" equalled total, so three tiles held one fact. A genuinely low cartridge already reaches the queue through the printers-supplies card, which is the moment anyone wants it. Three cards whose whole job was to display 0. Recent Devices: the largest block on the page, listing five machines that were all "In Use" with business unit "TBD". Recency is not a reason to look at something. CONTRACT 0.26.0: 'shortname' Cards may declare a short label for their rows in the merged queue. Their 'title' is written as a heading - "Endpoints with crashes or shutdowns" - and as a row badge it ran wider than the hostname it qualified, putting the badge rather than the asset at the start of every line. CSS-truncating the title was tried first and was worse: four consecutive rows reading "ENDPOINTS WITH CR..." say nothing at all. Optional and defaults to the title, so a plugin that does not declare one is unaffected. Additive field, minor bump. ALSO: "silent 1 days" A meta suffix is written for the plural case and the card cannot know the value. One row saying it was a shrug; the merged queue puts every quiet endpoint on one screen, so it said it five times in a column and read as broken. Now singularised at exactly 1, on the first word, keeping what follows: "1 event (48h)", "silent 1 day". The test asserting ' ms' survives is what caught the first version turning it into ' m'. DashboardRow.vue is extracted because the queue and the context panels render the same row, and the map tooltip, link fallbacks and chips are too easy to get subtly different in a second copy. Verified on screen against live data. Full suite 2178 passed, frontend 301.
This commit is contained in:
@@ -7,147 +7,168 @@
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- What needs a person, before the totals that are true every day.
|
||||
Cards are declared by plugins (get_dashboard_widgets) and rendered
|
||||
generically; an empty or failed card hides itself. See
|
||||
docs/proposals/dashboard-live-fleet.md. -->
|
||||
<DashboardCards />
|
||||
<!-- The board: a severity rollup, one worst-first queue of what needs a
|
||||
person, and a rail of context. Cards are declared by plugins
|
||||
(get_dashboard_widgets) and rendered generically; an empty or failed
|
||||
card hides itself. See docs/proposals/dashboard-live-fleet.md.
|
||||
|
||||
<!-- Inventory context, below the exceptions.
|
||||
These used to mix two different things without saying so: two tiles
|
||||
counted asset TYPES and two counted asset STATUSES, and the status
|
||||
one was labelled "Active", which reads as "not deleted" when it
|
||||
actually meant status = In Use across every type. Now every tile
|
||||
counts one thing and its label says which. -->
|
||||
<div class="dashboard-grid">
|
||||
<div class="stat-card">
|
||||
<div class="label">Machines</div>
|
||||
<div class="value">{{ stats.totalmachines || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">PCs</div>
|
||||
<div class="value">{{ stats.totalpc || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Printers</div>
|
||||
<div class="value">{{ stats.totalprinter || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Network devices</div>
|
||||
<div class="value">{{ stats.totalnetwork || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">All assets</div>
|
||||
<div class="value">{{ stats.totalassets || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="label">All assets in use</div>
|
||||
<div class="value">{{ stats.activeassets || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<div class="label">All assets in repair</div>
|
||||
<div class="value">{{ stats.inrepair || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
Inventory goes in the rail through the slot, because a count is not
|
||||
an exception and DashboardCards has no business declaring one. -->
|
||||
<DashboardCards>
|
||||
<template #rail>
|
||||
<section class="dc-panel estate">
|
||||
<header class="estate-head">
|
||||
<h3>Estate</h3>
|
||||
<router-link to="/reports" class="estate-link">Reports</router-link>
|
||||
</header>
|
||||
|
||||
<!-- Printer Stats -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Printers</h3>
|
||||
<router-link to="/printers" class="btn btn-secondary btn-sm">View All</router-link>
|
||||
</div>
|
||||
<!-- One line per asset type, then the total under a rule.
|
||||
These used to be seven equal tiles in which four of them summed
|
||||
to a fifth: machines + PCs + printers + network IS all assets,
|
||||
shown beside its own addends at the same weight. A total that
|
||||
looks like a sixth category invites people to add it in again.
|
||||
Two of the seven also counted STATUS rather than type without
|
||||
saying so. -->
|
||||
<div class="estate-row">
|
||||
<router-link to="/machines" class="estate-k">Machines</router-link>
|
||||
<span class="estate-v">{{ stats.totalmachines || 0 }}</span>
|
||||
</div>
|
||||
<div class="estate-row">
|
||||
<router-link to="/pcs" class="estate-k">PCs</router-link>
|
||||
<span class="estate-v">{{ stats.totalpc || 0 }}</span>
|
||||
</div>
|
||||
<div class="estate-row">
|
||||
<router-link to="/printers" class="estate-k">Printers</router-link>
|
||||
<span class="estate-v">{{ stats.totalprinter || 0 }}</span>
|
||||
</div>
|
||||
<div class="estate-row">
|
||||
<router-link to="/network" class="estate-k">Network devices</router-link>
|
||||
<span class="estate-v">{{ stats.totalnetwork || 0 }}</span>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="stat-card">
|
||||
<div class="label">Total Printers</div>
|
||||
<div class="value">{{ printerStats.totalprinters || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="label">Online</div>
|
||||
<div class="value">{{ printerStats.online || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<div class="label">Low Supplies</div>
|
||||
<div class="value">{{ printerStats.lowsupplies || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-card danger">
|
||||
<div class="label">Critical</div>
|
||||
<div class="value">{{ printerStats.criticalsupplies || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="estate-row estate-total">
|
||||
<span class="estate-k">All assets</span>
|
||||
<span class="estate-v">{{ stats.totalassets || 0 }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Recent Devices -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Recent Devices</h3>
|
||||
<router-link to="/machines" class="btn btn-secondary btn-sm">View All</router-link>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset #</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Business Unit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="machine in recentMachines" :key="machine.assetid">
|
||||
<td>{{ machine.assetnumber || machine.name || '-' }}</td>
|
||||
<td>{{ machine.assettypename || '-' }}</td>
|
||||
<td>{{ machine.statusname || '-' }}</td>
|
||||
<td>{{ machine.businessunitname || '-' }}</td>
|
||||
</tr>
|
||||
<tr v-if="recentMachines.length === 0">
|
||||
<td colspan="4" style="text-align: center; color: var(--text-light);">
|
||||
No devices found
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status, on one line, and explicitly labelled as status. In
|
||||
repair is muted at zero rather than given a card of its own to
|
||||
announce that nothing is wrong. -->
|
||||
<p class="estate-status">
|
||||
<span class="estate-inuse">{{ stats.activeassets || 0 }}</span> in use
|
||||
<span aria-hidden="true"> / </span>
|
||||
<span :class="{ 'estate-zero': !stats.inrepair }">
|
||||
{{ stats.inrepair || 0 }} in repair
|
||||
</span>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
</DashboardCards>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// WHAT THIS PAGE DROPPED, and why, so it does not come back by accident:
|
||||
//
|
||||
// The printer strip (Total / Online / Low Supplies / Critical). It repeated the
|
||||
// printer count a third time - the tile row and "Total Printers" were already
|
||||
// the same number - and "Online" equalled total, so three tiles carried one
|
||||
// fact. Supplies are already a dashboard card the printers plugin declares
|
||||
// (printers-supplies, severity warning), so a genuinely low cartridge reaches
|
||||
// the queue where it can be acted on, instead of sitting in a tile reading 0.
|
||||
//
|
||||
// Recent Devices. It was the largest block on the page and listed five machines
|
||||
// that were all "In Use" with business unit "TBD". Recency is not a reason to
|
||||
// look at something, and it cost more vertical space than everything that
|
||||
// needed a person put together. A filtered "recently changed" view belongs in
|
||||
// Reports.
|
||||
import DashboardCards from '@/components/DashboardCards.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { dashboardApi, assetsApi, printersApi } from '../api'
|
||||
import { dashboardApi } from '../api'
|
||||
|
||||
const loading = ref(true)
|
||||
const stats = ref({})
|
||||
const printerStats = ref({})
|
||||
const recentMachines = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [dashRes, machinesRes, printersRes] = await Promise.all([
|
||||
dashboardApi.summary().catch(() => ({ data: { data: {} } })),
|
||||
assetsApi.list({ perpage: 5 }).catch(() => ({ data: { data: [] } })),
|
||||
printersApi.dashboardSummary().catch(() => ({ data: { data: {} } }))
|
||||
])
|
||||
|
||||
stats.value = dashRes.data.data || {}
|
||||
recentMachines.value = machinesRes.data.data || []
|
||||
printerStats.value = printersRes.data.data || {}
|
||||
const response = await dashboardApi.summary().catch(() => ({ data: { data: {} } }))
|
||||
stats.value = response.data.data || {}
|
||||
} catch (error) {
|
||||
console.error('Dashboard load error:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function getStatusClass(status) {
|
||||
if (!status) return 'badge-info'
|
||||
const s = status.toLowerCase()
|
||||
if (s === 'in use' || s === 'active') return 'badge-success'
|
||||
if (s === 'in repair') return 'badge-warning'
|
||||
if (s === 'retired') return 'badge-danger'
|
||||
return 'badge-info'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Matches the panels in DashboardCards; the rail reads as one column rather
|
||||
than as a page card that happens to sit beside them. */
|
||||
.estate {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 0.85rem 1rem 0.9rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.estate-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.estate-head h3 {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-light);
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.estate-link {
|
||||
font-size: 0.78rem;
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
}
|
||||
.estate-link:hover { text-decoration: underline; }
|
||||
|
||||
.estate-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.35rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.estate-row:last-of-type { border-bottom: none; }
|
||||
|
||||
.estate-k {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
a.estate-k:hover { text-decoration: underline; }
|
||||
span.estate-k { color: var(--text); font-weight: 600; }
|
||||
|
||||
.estate-v {
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.estate-total {
|
||||
margin-top: 0.35rem;
|
||||
border-top: 2px solid var(--border);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.estate-status {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
.estate-inuse { color: var(--success); font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.estate-zero { color: var(--text-light); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user