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.
235 lines
9.1 KiB
Vue
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>
|