Merge main into wj before taking wilm's work
wj held nothing main lacked - git cherry marks every one of its commits as already present - but promotion cherry-picks, so the histories diverged and a fast-forward is not possible. Merging rather than resetting keeps the branch recoverable and needs no force-push. Levelling first so wilm's patches apply against current product code. # Conflicts: # CONTRIBUTING.md # frontend/src/views/Dashboard.vue # tools/wj-status.sh
This commit is contained in:
@@ -153,9 +153,15 @@ Tests are required for:
|
||||
|
||||
## 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.
|
||||
`main` is the product every site runs and the publication mirror exports.
|
||||
`<site>` branches hold work that is not ready for other sites to see. There are
|
||||
two: `wj` here, and `wilm` on the corporate GitHub, where the other site
|
||||
develops the .NET endpoint agent.
|
||||
|
||||
A site branch is not a fork. Both feed `main`, and the sooner they do it the
|
||||
cheaper it is. A branch the dev box cannot see is still a branch: `wilm` is not
|
||||
visible from here because this machine has no GitHub account, which is why
|
||||
`fetch-wilm-branch.cmd` on the share exists to bring it back for review.
|
||||
|
||||
**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
|
||||
@@ -188,10 +194,9 @@ 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.
|
||||
The real risk is product work sitting on a site branch until bringing it back is
|
||||
a port rather than a merge - one such round trip cost a MySQL dialect fix, a
|
||||
contract violation and a plugin install.
|
||||
|
||||
So rebase the site branch on `main` often, and promote as you go rather than in
|
||||
one batch at the end.
|
||||
|
||||
@@ -90,6 +90,6 @@ Manifest-less directories under `plugins/` are core frontend surface and always
|
||||
|
||||
## Size
|
||||
|
||||
- test functions defined: **1503** (parametrised cases collect higher)
|
||||
- test functions defined: **1507** (parametrised cases collect higher)
|
||||
- documented API paths: **333** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`)
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
<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
|
||||
that rendered as a tidy column of zeros - a site with nothing
|
||||
in it, 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>
|
||||
<section v-if="summaryloaded" class="dc-panel assets">
|
||||
<header class="assets-head">
|
||||
<h3>Assets</h3>
|
||||
<router-link to="/reports" class="assets-link">Reports</router-link>
|
||||
</header>
|
||||
|
||||
<!-- One line per asset type, then the total under a rule.
|
||||
@@ -37,21 +37,21 @@
|
||||
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 class="assets-row">
|
||||
<router-link to="/machines" class="assets-k">Machines</router-link>
|
||||
<span class="assets-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 class="assets-row">
|
||||
<router-link to="/pcs" class="assets-k">PCs</router-link>
|
||||
<span class="assets-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 class="assets-row">
|
||||
<router-link to="/printers" class="assets-k">Printers</router-link>
|
||||
<span class="assets-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 class="assets-row">
|
||||
<router-link to="/network" class="assets-k">Network devices</router-link>
|
||||
<span class="assets-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
|
||||
@@ -59,24 +59,34 @@
|
||||
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 class="assets-row">
|
||||
<router-link to="/measuringtools" class="assets-k">Measuring tools</router-link>
|
||||
<span class="assets-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 class="assets-row assets-total">
|
||||
<span class="assets-k">All assets</span>
|
||||
<span class="assets-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
|
||||
<!-- Every status the total contains, not just In Use. These were
|
||||
counted server-side for the rollup and then thrown away, so a
|
||||
reader could see one number and had no way to learn where the
|
||||
rest of the total went. Retired assets in particular ARE in
|
||||
the total, which is not guessable. -->
|
||||
<div v-for="row in statusrows" :key="row.name" class="assets-row assets-sub">
|
||||
<span class="assets-k assets-subk">{{ row.name }}</span>
|
||||
<span class="assets-v assets-subv">{{ row.count }}</span>
|
||||
</div>
|
||||
|
||||
<!-- What the total covers. Asked twice what the number meant, which
|
||||
is the signal a label is underspecified. -->
|
||||
<p class="assets-caption">
|
||||
Active records only<span v-if="stats.dualpathcollapsed">, dual-bay
|
||||
pairs counted once</span>.
|
||||
<span v-if="stats.inactiveexcluded">
|
||||
{{ stats.inactiveexcluded }} deactivated record{{ stats.inactiveexcluded === 1 ? '' : 's' }}
|
||||
excluded.
|
||||
</span>
|
||||
</p>
|
||||
</section>
|
||||
@@ -89,12 +99,12 @@
|
||||
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">
|
||||
<section v-else-if="openable.length" class="dc-panel assets">
|
||||
<header class="assets-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 v-for="item in openable" :key="item.route" class="assets-row">
|
||||
<router-link :to="item.route" class="assets-k">{{ item.name }}</router-link>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -133,6 +143,17 @@ const summaryloaded = ref(false)
|
||||
// hold - and to what the guest role reaches, when nobody is signed in.
|
||||
const navitems = ref([])
|
||||
|
||||
// Every status in the total, biggest first. Sorted rather than fixed-order
|
||||
// because the statuses are seed data a site can change - hardcoding "In Use,
|
||||
// In Repair, Retired" would silently omit a status somebody added.
|
||||
const statusrows = computed(() => {
|
||||
const statuses = stats.value.statuses || {}
|
||||
return Object.keys(statuses)
|
||||
.map((name) => ({ name, count: statuses[name] }))
|
||||
.filter((row) => row.count > 0)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
})
|
||||
|
||||
const openable = computed(
|
||||
// The dashboard itself is not somewhere to go from the dashboard.
|
||||
() => navitems.value.filter((item) => item.route && item.route !== '/'))
|
||||
@@ -165,7 +186,7 @@ onMounted(async () => {
|
||||
<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 {
|
||||
.assets {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
@@ -173,13 +194,13 @@ onMounted(async () => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.estate-head {
|
||||
.assets-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.estate-head h3 {
|
||||
.assets-head h3 {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
@@ -188,14 +209,14 @@ onMounted(async () => {
|
||||
color: var(--text-light);
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.estate-link {
|
||||
.assets-link {
|
||||
font-size: 0.78rem;
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
}
|
||||
.estate-link:hover { text-decoration: underline; }
|
||||
.assets-link:hover { text-decoration: underline; }
|
||||
|
||||
.estate-row {
|
||||
.assets-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
@@ -203,32 +224,39 @@ onMounted(async () => {
|
||||
padding: 0.35rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.estate-row:last-of-type { border-bottom: none; }
|
||||
.assets-row:last-of-type { border-bottom: none; }
|
||||
|
||||
.estate-k {
|
||||
.assets-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; }
|
||||
a.assets-k:hover { text-decoration: underline; }
|
||||
span.assets-k { color: var(--text); font-weight: 600; }
|
||||
|
||||
.estate-v {
|
||||
.assets-v {
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.estate-total {
|
||||
.assets-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;
|
||||
/* Status rows sit UNDER the total, indented and quieter, because they break
|
||||
the total down rather than adding to it. Same weight as the type rows would
|
||||
read as more things to add up. */
|
||||
.assets-sub { border-bottom: none; padding: 0.15rem 0 0.15rem 0.75rem; }
|
||||
.assets-subk { color: var(--text-light); font-size: 0.85rem; }
|
||||
a.assets-subk:hover { text-decoration: none; }
|
||||
.assets-subv { font-weight: 500; font-size: 0.85rem; color: var(--text-light); }
|
||||
|
||||
.assets-caption {
|
||||
margin: 0.6rem 0 0;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
color: var(--text-light);
|
||||
}
|
||||
.estate-inuse { color: var(--success); font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.estate-zero { color: var(--text-light); }
|
||||
</style>
|
||||
|
||||
@@ -38,6 +38,19 @@ def _count_by_type(assettype):
|
||||
return query.count()
|
||||
|
||||
|
||||
def _inactive_excluded():
|
||||
"""Counted-type assets held back by isactive, i.e. soft-deleted.
|
||||
|
||||
Reported so the page can account for itself. A total that silently drops
|
||||
rows is the reason somebody compares it with a list page and finds a
|
||||
difference they cannot explain.
|
||||
"""
|
||||
return (db.session.query(Asset).join(AssetType)
|
||||
.filter(Asset.isactive == False,
|
||||
AssetType.assettype.in_(COUNTEDTYPES))
|
||||
.count())
|
||||
|
||||
|
||||
COUNTEDTYPES = ('machine', 'computer', 'network_device', 'printer',
|
||||
'measuring_tool')
|
||||
|
||||
@@ -69,6 +82,10 @@ def _countedassets():
|
||||
@require_permission('assets.view')
|
||||
def get_dashboard():
|
||||
"""Get dashboard summary data (asset-based)."""
|
||||
# Imported here, as in the helpers above: the dualpath service reaches back
|
||||
# into models, so a module-level import is circular.
|
||||
from shopdb.core.services.dualpath import dualpath_single_machine_enabled
|
||||
|
||||
machine_count = _count_by_type('machine')
|
||||
pc_count = _count_by_type('computer')
|
||||
network_count = _count_by_type('network_device')
|
||||
@@ -99,6 +116,22 @@ def get_dashboard():
|
||||
'totalmeasuringtool': measuringtool_count,
|
||||
'activeassets': status_dict.get('In Use', 0),
|
||||
'inrepair': status_dict.get('In Repair', 0),
|
||||
# What the totals actually cover, so the page can say so rather than
|
||||
# leaving a reader to work it out from a number that surprised them.
|
||||
# Every count here filters isactive, and the dual-path collapse is a
|
||||
# per-site toggle - asserting it unconditionally would be false at a
|
||||
# site that has it off.
|
||||
'countsactiveonly': True,
|
||||
'dualpathcollapsed': bool(dualpath_single_machine_enabled()),
|
||||
# Every status, not just the two that had callers. The counts were
|
||||
# already built for the rollup above and then thrown away, so a reader
|
||||
# could see "In Use" and had no way to learn where the rest went.
|
||||
'statuses': status_dict,
|
||||
# A DIFFERENT thing to a status, and the one that makes a total
|
||||
# disagree with a list page. isactive is the soft-delete flag: these
|
||||
# rows are excluded from every count above, so they are in no status
|
||||
# bucket and cannot be found by adding the buckets up.
|
||||
'inactiveexcluded': _inactive_excluded(),
|
||||
# Structured data
|
||||
'counts': {
|
||||
'machines': machine_count,
|
||||
|
||||
@@ -261,6 +261,31 @@ echo "bundle: $BUNDLE"
|
||||
# --- 6. optional per-instance dists (both bases: /ops = dev, /shopdb = prod) ---
|
||||
# Two instances run on the box; each needs its own base-path build. Always
|
||||
# rebuild BOTH so prod never ships a stale frontend.
|
||||
# --dist IS MAIN ONLY.
|
||||
#
|
||||
# These paths are not branch-qualified and cannot safely be: update-prod-server
|
||||
# .cmd robocopies frontend-dist-subpath-shopdb onto the production server with
|
||||
# /MIR, a full mirror, and has no idea which branch built it. So --dist from a
|
||||
# site branch would replace production's frontend with unreleased work at the
|
||||
# next deploy, silently, and the deploy script could not tell.
|
||||
#
|
||||
# Branch-qualifying the directory would leave the same trap for whoever pointed
|
||||
# a deploy at the wrong one. Production runs the product; a site branch has no
|
||||
# business producing the artefact production installs.
|
||||
if [ "$BUILD_DIST" = 1 ] && [ "$BRANCH" != "main" ]; then
|
||||
echo "REFUSED: --dist on branch '$BRANCH'." >&2
|
||||
echo >&2
|
||||
echo "The dist directories are what update-prod-server.cmd mirrors onto the" >&2
|
||||
echo "production server, and they are not branch-qualified. Building them" >&2
|
||||
echo "from '$BRANCH' would put unreleased work on production at the next" >&2
|
||||
echo "deploy, with nothing to reveal it." >&2
|
||||
echo >&2
|
||||
echo "Promote the commits to main and export from there:" >&2
|
||||
echo " tools/wj-status.sh" >&2
|
||||
echo " tools/promote-to-main.sh <sha> ..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$BUILD_DIST" = 1 ]; then
|
||||
cd "$WORK/frontend"
|
||||
for base in ops shopdb; do
|
||||
|
||||
@@ -44,10 +44,25 @@ if ! git rev-parse --verify --quiet refs/heads/wj >/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMMITS="$(git rev-list --reverse "$RANGE")"
|
||||
# git cherry, not rev-list. Promotion CHERRY-PICKS, which gives the commit a new
|
||||
# sha on main, so `main..wj` keeps listing work that already landed - and this
|
||||
# tool would then invite you to promote it a second time. `git cherry` compares
|
||||
# patch content and marks an already-present commit with a leading '-'.
|
||||
COMMITS="$(git cherry main wj | awk '$1 == "+" { print $2 }')"
|
||||
|
||||
ALREADY="$(git cherry main wj | awk '$1 == "-" { print $2 }' | wc -l)"
|
||||
|
||||
if [ -z "$COMMITS" ]; then
|
||||
echo "Nothing on wj that main does not have."
|
||||
if [ "$ALREADY" -gt 0 ]; then
|
||||
echo "($ALREADY commit(s) here have already landed on main as cherry-picks.)"
|
||||
fi
|
||||
echo
|
||||
echo "wj is behind main by:"
|
||||
git log --oneline wj..main | sed 's/^/ /' | head -10
|
||||
echo
|
||||
echo "Rebase it before starting new work:"
|
||||
echo " git checkout wj && git rebase main"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user