diff --git a/.gitattributes b/.gitattributes index e03d96cd..9a0a204a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db0f47aa..654127ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. `` 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//`, 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 # what files would reach main +tools/promote-to-main.sh [ ...] # 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 diff --git a/frontend/src/components/DashboardCards.vue b/frontend/src/components/DashboardCards.vue index 4ce82ba4..076b7887 100644 --- a/frontend/src/components/DashboardCards.vue +++ b/frontend/src/components/DashboardCards.vue @@ -1,5 +1,5 @@ @@ -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 })