Files
shopdb-flask/frontend/src/views/Dashboard.vue
Cam Proudlock 43fed63277 Site branch tooling, branch-aware publication, dashboard fixes
Publication now mirrors the branch and writes one bundle per branch. Dashboard:
guests see what they can reach rather than a blank page, and the estate total
lists every type it counts.
2026-08-30 11:19:00 -04:00

235 lines
9.1 KiB
Vue

<template>
<div>
<div class="page-header">
<h2>Dashboard</h2>
</div>
<div v-if="loading" class="loading">Loading...</div>
<template v-else>
<!-- 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 goes in the rail through the slot, because a count is not
an exception and DashboardCards has no business declaring one. -->
<DashboardCards>
<template #rail>
<!-- Only when the totals actually loaded. /api/dashboard/summary
needs assets.view, and a caller without it got an empty object
that rendered as a tidy column of zeros - an estate of nothing,
stated with total confidence. Same fault as reporting Low
Supplies 0 while the monitoring is down. -->
<section v-if="summaryloaded" class="dc-panel estate">
<header class="estate-head">
<h3>Estate</h3>
<router-link to="/reports" class="estate-link">Reports</router-link>
</header>
<!-- One line per asset type, then the total under a rule.
These used to be seven equal tiles in which some of them summed
to another: the type counts ARE all assets, shown beside their
own total at the same weight. A total that looks like another
category invites people to add it in again. Two of the seven
also counted STATUS rather than type without saying so.
EVERY counted type must appear here. The endpoint's grand total
is machines + PCs + network + printers + measuring tools, and
omitting one leaves a total nothing on screen explains. -->
<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>
<!-- Measuring tools are IN the grand total the endpoint returns, so
they have to be visible here. Left out, the total silently
stopped matching the rows above it - 610 of rows over a stated
729 on an instance with gages - which is the exact fault this
panel was built to remove. Dev has none, so it looked correct
here and was wrong everywhere the import had run. -->
<div class="estate-row">
<router-link to="/measuringtools" class="estate-k">Measuring tools</router-link>
<span class="estate-v">{{ stats.totalmeasuringtool || 0 }}</span>
</div>
<div class="estate-row estate-total">
<span class="estate-k">All assets</span>
<span class="estate-v">{{ stats.totalassets || 0 }}</span>
</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>
<!-- Never leave the page blank, and do not apologise either: show
what this reader CAN open. A guest who can see none of the cards
and cannot read the totals previously got a heading and nothing
else, which reads as a broken site rather than a closed door.
The list is the same permission-filtered navigation the sidebar
uses - server-filtered for a signed-out visitor - so it can never
offer a link that dead-ends at the router guard, and it names no
plugin this page had to know about. -->
<section v-else-if="openable.length" class="dc-panel estate">
<header class="estate-head">
<h3>Where you can go</h3>
</header>
<div v-for="item in openable" :key="item.route" class="estate-row">
<router-link :to="item.route" class="estate-k">{{ item.name }}</router-link>
</div>
</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, computed, onMounted } from 'vue'
import { dashboardApi } from '../api'
const loading = ref(true)
const stats = ref({})
// Whether the totals actually came back. Swallowing the error and rendering
// stats as {} turned a 403 into a column of confident zeros.
const summaryloaded = ref(false)
// Where this reader can actually go, for the case where the board has nothing
// for them. Same source as the sidebar, so it is already filtered to what they
// hold - and to what the guest role reaches, when nobody is signed in.
const navitems = ref([])
const openable = computed(
// The dashboard itself is not somewhere to go from the dashboard.
() => navitems.value.filter((item) => item.route && item.route !== '/'))
onMounted(async () => {
try {
const response = await dashboardApi.summary()
stats.value = response.data.data || {}
summaryloaded.value = Object.keys(stats.value).length > 0
} catch (error) {
// A caller without assets.view is restricted, not broken, so this is not
// logged as an error - the panel offers them somewhere to go instead.
summaryloaded.value = false
}
if (!summaryloaded.value) {
// Only fetched when it is needed, so the common case costs nothing.
try {
const response = await dashboardApi.navigation()
navitems.value = response.data.data || []
} catch (error) {
navitems.value = []
}
}
loading.value = false
})
</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>