diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 654127ef..848e3026 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. `` 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. +`` 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. diff --git a/docs/PROJECT-MAP.md b/docs/PROJECT-MAP.md index 507b501f..fc7e4b1e 100644 --- a/docs/PROJECT-MAP.md +++ b/docs/PROJECT-MAP.md @@ -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`) diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 703a0db4..1555f7c3 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -18,13 +18,13 @@ @@ -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 () => { diff --git a/shopdb/core/api/dashboard.py b/shopdb/core/api/dashboard.py index d8350d15..5d197e22 100644 --- a/shopdb/core/api/dashboard.py +++ b/shopdb/core/api/dashboard.py @@ -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, diff --git a/tools/export-github.sh b/tools/export-github.sh index 7c4ca2a5..baf9c824 100755 --- a/tools/export-github.sh +++ b/tools/export-github.sh @@ -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 ..." >&2 + exit 1 +fi + if [ "$BUILD_DIST" = 1 ]; then cd "$WORK/frontend" for base in ops shopdb; do diff --git a/tools/wj-status.sh b/tools/wj-status.sh index d133cd53..f35fe110 100755 --- a/tools/wj-status.sh +++ b/tools/wj-status.sh @@ -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