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.
This commit is contained in:
6
.gitattributes
vendored
6
.gitattributes
vendored
@@ -4,3 +4,9 @@
|
||||
*.sh text eol=lf
|
||||
scripts/check-naming-and-style.sh text eol=lf
|
||||
.githooks/pre-commit text eol=lf
|
||||
|
||||
# Batch files must be CRLF. cmd.exe parses a lone LF inconsistently around
|
||||
# labels and goto, so an LF-only .cmd can run its happy path and then jump
|
||||
# nowhere on the error path - which is the path that matters in a promote
|
||||
# script.
|
||||
*.cmd text eol=crlf
|
||||
|
||||
@@ -151,6 +151,51 @@ Tests are required for:
|
||||
- New plugin hooks (contract test)
|
||||
- Schema migrations (before/after data integrity)
|
||||
|
||||
## Branches: what belongs on main, and what does not
|
||||
|
||||
Two branches. `main` is the product every site runs and the publication mirror
|
||||
exports. `<site>` branches - currently `wj` - hold work that is not ready for
|
||||
other sites to see.
|
||||
|
||||
**Most work is product.** ADR-015 gives site differences three homes on `main`:
|
||||
a setting with a neutral default, seed data, or a site-namespaced directory
|
||||
(`scripts/site_imports/<site>/`, which the style gate exempts). That is the only
|
||||
site-specific home in the tree, so a site branch is about TIMING, not locality:
|
||||
things that are not ready yet, rather than things that are permanently ours.
|
||||
|
||||
Say which at commit time, with a trailer, because the decision does not survive
|
||||
being deferred - months later it is a list of subject lines and a guess:
|
||||
|
||||
```bash
|
||||
git commit -m "Add the thing" -m "Scope: product" # main should have it
|
||||
git commit -m "Add the thing" -m "Scope: site" # this site alone
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
tools/wj-status.sh # what is unpromoted, and where it belongs
|
||||
tools/promote-to-main.sh --dry-run <sha> # what files would reach main
|
||||
tools/promote-to-main.sh <sha> [<sha> ...] # oldest first; runs main's gates
|
||||
tools/git-push.sh main # you push, deliberately
|
||||
```
|
||||
|
||||
Windows equivalents are `tools\git-push.cmd` and `tools\promote-to-main.cmd`.
|
||||
|
||||
Promotion cherry-picks NAMED commits rather than merging the branch. Merging
|
||||
takes the site work along with the product work, which is how a site branch
|
||||
quietly becomes the product.
|
||||
|
||||
**The failure to watch for is not what you think.** Promoting something
|
||||
site-specific by accident is caught by the ADR-015 gate on the way into `main`.
|
||||
The real risk is product work sitting on a site branch forever because nobody
|
||||
decided - which is how the `wilm` branch stopped being a merge and became a
|
||||
port, costing a MySQL dialect fix, a contract violation and a plugin install to
|
||||
bring back.
|
||||
|
||||
So rebase the site branch on `main` often, and promote as you go rather than in
|
||||
one batch at the end.
|
||||
|
||||
## Code review checklist
|
||||
|
||||
- [ ] No underscores in table or column names
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="hasBoard" class="dc-board">
|
||||
<div class="dc-board">
|
||||
|
||||
<!-- The rollup. Counts ROWS, not cards, and each count filters the queue
|
||||
below rather than navigating away: landing on a number that tells you
|
||||
@@ -54,7 +54,8 @@
|
||||
|
||||
<!-- ONE queue, worst first, instead of a card per source. Reading order
|
||||
is work order. -->
|
||||
<section v-if="queue.length || overflow.length" class="dc-panel dc-queue">
|
||||
<section v-if="canseechecks && (queue.length || overflow.length)"
|
||||
class="dc-panel dc-queue">
|
||||
<header class="dc-panel-head">
|
||||
<h3>Needs attention</h3>
|
||||
<span class="dc-panel-count">{{ queueTotal }}</span>
|
||||
@@ -78,7 +79,12 @@
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section v-else class="dc-panel dc-clear">
|
||||
<!-- Only claim the board is clear to somebody who can actually see the
|
||||
checks. A reader with no permission for any of them is not looking at
|
||||
a healthy fleet, they are looking at nothing - and telling them
|
||||
"nothing needs attention" is the same lie as reporting Low Supplies 0
|
||||
while the monitoring is down. -->
|
||||
<section v-else-if="canseechecks" class="dc-panel dc-clear">
|
||||
<p class="dc-clear-line">Nothing needs attention.</p>
|
||||
</section>
|
||||
|
||||
@@ -195,7 +201,11 @@ const context = computed(
|
||||
const queueTotal = computed(
|
||||
() => queue.value.length + overflow.value.reduce((sum, e) => sum + e.hidden, 0))
|
||||
|
||||
const hasBoard = computed(() => liveCards.value.length > 0)
|
||||
// How many cards this caller is ALLOWED to see, independent of whether they
|
||||
// currently hold rows. Zero means no visibility into exceptions at all, which
|
||||
// must not be drawn as "nothing wrong".
|
||||
const permittedcount = ref(0)
|
||||
const canseechecks = computed(() => permittedcount.value > 0)
|
||||
|
||||
function toggleFilter(level) {
|
||||
filter.value = filter.value === level ? '' : level
|
||||
@@ -237,6 +247,8 @@ async function load() {
|
||||
const wanted = renderableCards(
|
||||
permittedCards(declared, (name) => auth.hasPermission(name)))
|
||||
|
||||
permittedcount.value = wanted.length
|
||||
|
||||
cards.value = await Promise.all(wanted.map(async (card) => {
|
||||
try {
|
||||
const response = await api.get(toApiPath(card.endpoint))
|
||||
|
||||
@@ -16,19 +16,27 @@
|
||||
an exception and DashboardCards has no business declaring one. -->
|
||||
<DashboardCards>
|
||||
<template #rail>
|
||||
<section class="dc-panel estate">
|
||||
<!-- 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 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. -->
|
||||
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>
|
||||
@@ -45,6 +53,16 @@
|
||||
<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>
|
||||
@@ -62,6 +80,23 @@
|
||||
</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>
|
||||
@@ -84,21 +119,46 @@
|
||||
// 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 { 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().catch(() => ({ data: { data: {} } }))
|
||||
const response = await dashboardApi.summary()
|
||||
stats.value = response.data.data || {}
|
||||
summaryloaded.value = Object.keys(stats.value).length > 0
|
||||
} catch (error) {
|
||||
console.error('Dashboard load error:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
// 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user