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
|
*.sh text eol=lf
|
||||||
scripts/check-naming-and-style.sh text eol=lf
|
scripts/check-naming-and-style.sh text eol=lf
|
||||||
.githooks/pre-commit 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)
|
- New plugin hooks (contract test)
|
||||||
- Schema migrations (before/after data integrity)
|
- 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
|
## Code review checklist
|
||||||
|
|
||||||
- [ ] No underscores in table or column names
|
- [ ] No underscores in table or column names
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="hasBoard" class="dc-board">
|
<div class="dc-board">
|
||||||
|
|
||||||
<!-- The rollup. Counts ROWS, not cards, and each count filters the queue
|
<!-- The rollup. Counts ROWS, not cards, and each count filters the queue
|
||||||
below rather than navigating away: landing on a number that tells you
|
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
|
<!-- ONE queue, worst first, instead of a card per source. Reading order
|
||||||
is work 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">
|
<header class="dc-panel-head">
|
||||||
<h3>Needs attention</h3>
|
<h3>Needs attention</h3>
|
||||||
<span class="dc-panel-count">{{ queueTotal }}</span>
|
<span class="dc-panel-count">{{ queueTotal }}</span>
|
||||||
@@ -78,7 +79,12 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</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>
|
<p class="dc-clear-line">Nothing needs attention.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -195,7 +201,11 @@ const context = computed(
|
|||||||
const queueTotal = computed(
|
const queueTotal = computed(
|
||||||
() => queue.value.length + overflow.value.reduce((sum, e) => sum + e.hidden, 0))
|
() => 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) {
|
function toggleFilter(level) {
|
||||||
filter.value = filter.value === level ? '' : level
|
filter.value = filter.value === level ? '' : level
|
||||||
@@ -237,6 +247,8 @@ async function load() {
|
|||||||
const wanted = renderableCards(
|
const wanted = renderableCards(
|
||||||
permittedCards(declared, (name) => auth.hasPermission(name)))
|
permittedCards(declared, (name) => auth.hasPermission(name)))
|
||||||
|
|
||||||
|
permittedcount.value = wanted.length
|
||||||
|
|
||||||
cards.value = await Promise.all(wanted.map(async (card) => {
|
cards.value = await Promise.all(wanted.map(async (card) => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(toApiPath(card.endpoint))
|
const response = await api.get(toApiPath(card.endpoint))
|
||||||
|
|||||||
@@ -16,19 +16,27 @@
|
|||||||
an exception and DashboardCards has no business declaring one. -->
|
an exception and DashboardCards has no business declaring one. -->
|
||||||
<DashboardCards>
|
<DashboardCards>
|
||||||
<template #rail>
|
<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">
|
<header class="estate-head">
|
||||||
<h3>Estate</h3>
|
<h3>Estate</h3>
|
||||||
<router-link to="/reports" class="estate-link">Reports</router-link>
|
<router-link to="/reports" class="estate-link">Reports</router-link>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- One line per asset type, then the total under a rule.
|
<!-- One line per asset type, then the total under a rule.
|
||||||
These used to be seven equal tiles in which four of them summed
|
These used to be seven equal tiles in which some of them summed
|
||||||
to a fifth: machines + PCs + printers + network IS all assets,
|
to another: the type counts ARE all assets, shown beside their
|
||||||
shown beside its own addends at the same weight. A total that
|
own total at the same weight. A total that looks like another
|
||||||
looks like a sixth category invites people to add it in again.
|
category invites people to add it in again. Two of the seven
|
||||||
Two of the seven also counted STATUS rather than type without
|
also counted STATUS rather than type without saying so.
|
||||||
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">
|
<div class="estate-row">
|
||||||
<router-link to="/machines" class="estate-k">Machines</router-link>
|
<router-link to="/machines" class="estate-k">Machines</router-link>
|
||||||
<span class="estate-v">{{ stats.totalmachines || 0 }}</span>
|
<span class="estate-v">{{ stats.totalmachines || 0 }}</span>
|
||||||
@@ -45,6 +53,16 @@
|
|||||||
<router-link to="/network" class="estate-k">Network devices</router-link>
|
<router-link to="/network" class="estate-k">Network devices</router-link>
|
||||||
<span class="estate-v">{{ stats.totalnetwork || 0 }}</span>
|
<span class="estate-v">{{ stats.totalnetwork || 0 }}</span>
|
||||||
</div>
|
</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">
|
<div class="estate-row estate-total">
|
||||||
<span class="estate-k">All assets</span>
|
<span class="estate-k">All assets</span>
|
||||||
@@ -62,6 +80,23 @@
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</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>
|
</template>
|
||||||
</DashboardCards>
|
</DashboardCards>
|
||||||
</template>
|
</template>
|
||||||
@@ -84,21 +119,46 @@
|
|||||||
// needed a person put together. A filtered "recently changed" view belongs in
|
// needed a person put together. A filtered "recently changed" view belongs in
|
||||||
// Reports.
|
// Reports.
|
||||||
import DashboardCards from '@/components/DashboardCards.vue'
|
import DashboardCards from '@/components/DashboardCards.vue'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { dashboardApi } from '../api'
|
import { dashboardApi } from '../api'
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const stats = ref({})
|
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 () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await dashboardApi.summary().catch(() => ({ data: { data: {} } }))
|
const response = await dashboardApi.summary()
|
||||||
stats.value = response.data.data || {}
|
stats.value = response.data.data || {}
|
||||||
|
summaryloaded.value = Object.keys(stats.value).length > 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Dashboard load error:', error)
|
// A caller without assets.view is restricted, not broken, so this is not
|
||||||
} finally {
|
// logged as an error - the panel offers them somewhere to go instead.
|
||||||
loading.value = false
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user