T5.3 - B4 surface: a pipeline strip, four server counts, four shareable links
Four cells on the launcher - Work packages, Release ready, On hold, Overdue -
every number from /api/wps/metrics, which T4.1 built. There is deliberately no
cache fallback anywhere in renderPipeline(): a remembered number sitting beside
three live ones is the failure B4 exists to remove, wearing a different hat.
The cells are the four the endpoint already computes and the dashboard already
filters on, so they map 1:1 onto its flags rather than inventing a fifth slice
nothing downstream understands. wp-creation-app.js now names them (DASH_FLAGS)
for the same reason: a cell linking to a filter the board does not recognise is
a dead link that still looks live.
"Links to a filtered view via a shareable URL" needed the filter to BE URL state,
which it was not - the dashboard kept its flag in a variable. So:
- dashToggleFlag pushes ?flag=<f>, and clears it on the way out of the board
- the creator applies ?flag= before its first render, not after (applying it
after paints the whole board and throws it away)
- Back and Forward move through filters like any other state
- work-package-suite-app.js forwards the flag ACROSS the iframe boundary, since
the creator's src carries only the project. B7/T7.1 dissolves that frame and
this hand-off goes with it; it is commented as such.
Zero is a real answer for one slice of a project that has work in it. Four zeros
on a project with none is not a reading, it is a strip that looks broken - that
case gets a sentence and a way into the creator instead. A failed request gets an
explicit error naming the failure, and no cells at all.
html/index.html the strip, its states, PIPE_CELLS
html/wp-creation-app.js flag as URL state; DASH_FLAGS; dashApplyFlag
html/work-package-suite-app.js forward the flag into the frame; clear on exit
tests/pipeline_check.py new - 43 checks
Done when
[x] every number comes from a server endpoint - proved by poisoning localStorage
with 99 fake packages and demanding the strip still read the server's 4
[x] each cell links to a filtered view via a shareable URL - and the probe
FOLLOWS the link and reads the filter inside the frame rather than trusting
that a correct-looking URL was built
[x] a project with zero work packages renders a sensible empty state
[x] the strip announces updates via aria-live (polite - a count is not an
interruption) and reports aria-busy while it is counting
What the probe caught
The link landed on "Complete the SOP Configuration first". Not the strip's
fault: browser_check.py's fixture stores a bare {governance: …} blob as the SOP
data, where production stores {sop, state}. restoreSavedSOP() needs `state` and
bails without it, so sopComplete stays false and the WP tab shows its gate.
pipeline_check seeds the production shape. The underlying wart is real and is
logged rather than fixed - see BL-018.
Verified one at a time
pipeline_check 43/43 new
launcher_check 58/58
stepper_check 70/70
url_state 23/23 the dashboard's new flag state did not disturb it
aggregates 16/16
browser_check 71/71
a11y 22/22
autosave 34/34
f_items F1-F5 FIXED, F6 REPRODUCES (T7.2)
No colour literal added: still 0 across all page sheets and inline blocks. The
four cells are told apart by a label, a sentence and an accent - three channels,
so colour is not carrying it alone (C1).
Raised, not fixed
BL-018 The WP tab's gate is the last localStorage-derived status in the app.
T4.1 moved the launcher's card to the server; the wizard page still
decides gate-or-creator from wp_suite_sop_complete plus a state blob.
pullProject refreshes both on load so a connected user is fine, but the
two answers come from different places and the fallback is silent.
Includes a second, sharper edge: project-data.js:210 writes that flag
for ANY row returned, including one with no `state` to restore - so the
flag is written and never read consistently. T7.1 owns it.
Question for the PR, per CLAUDE.md: the strip counts Overdue against `data.due`,
which is free text today. CR-004/CR-018 restructure location but not dates. If
"overdue" is going to drive anything beyond a launcher tile, that field needs a
type.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1828,10 +1828,39 @@ function dashRefreshAfterWrite(){
|
||||
done();
|
||||
}
|
||||
|
||||
function dashToggleFlag(f){
|
||||
// The four flags the dashboard filters on. Named here because the launcher's
|
||||
// pipeline strip (T5.3 / B4) links straight to them, and a cell that links to a
|
||||
// filter this file does not recognise is a dead link that still looks live.
|
||||
const DASH_FLAGS = ['ready', 'onhold', 'overdue', 'mine'];
|
||||
|
||||
function dashToggleFlag(f, opts){
|
||||
if(f==='all'){ dashFilter={status:'',discipline:'',q:'',flag:''}; }
|
||||
else { dashFilter.flag = dashFilter.flag===f ? '' : f; }
|
||||
dashPage=0; renderDashboard();
|
||||
dashPage=0;
|
||||
// S3: which slice of the board you are looking at is state, so it belongs in
|
||||
// the URL. Without this a pipeline cell could open the filter but nobody could
|
||||
// send anyone the result — which is the half of T5.3 that is about sharing.
|
||||
if(!(opts && opts.fromUrl)) urlSyncDashFlag();
|
||||
renderDashboard();
|
||||
}
|
||||
|
||||
function urlSyncDashFlag(opts){
|
||||
if(typeof WPUrl === 'undefined') return;
|
||||
const patch = { view: 'dashboard', wp: '', flag: dashFilter.flag || '' };
|
||||
if(activeProjectId) patch.project = activeProjectId;
|
||||
(opts && opts.replace ? WPUrl.replace : WPUrl.push).call(WPUrl, patch);
|
||||
}
|
||||
|
||||
// Apply a flag arriving from outside — a URL, or the embedding shell forwarding
|
||||
// one across the iframe boundary. Never pushes history: the caller's own
|
||||
// navigation is what put us here.
|
||||
function dashApplyFlag(f){
|
||||
const next = DASH_FLAGS.indexOf(f) >= 0 ? f : '';
|
||||
if(dashFilter.flag === next) return next;
|
||||
dashFilter.flag = next;
|
||||
dashPage = 0;
|
||||
if(currentView === 'Dashboard') renderDashboard();
|
||||
return next;
|
||||
}
|
||||
function dashSetStatus(s){ dashFilter.status = dashFilter.status===s ? '' : s; dashPage=0; renderDashboard(); }
|
||||
|
||||
@@ -1889,7 +1918,7 @@ function showDashboard(opts){
|
||||
const dv=document.getElementById('dashboard-view'); if(dv) dv.style.display='';
|
||||
currentView='Dashboard'; cmtUpdateCurStep();
|
||||
if(typeof WPUrl !== 'undefined' && !fromUrl){
|
||||
const patch = { view: 'dashboard', wp: '' };
|
||||
const patch = { view: 'dashboard', wp: '', flag: dashFilter.flag || '' };
|
||||
if(activeProjectId) patch.project = activeProjectId;
|
||||
WPUrl.push(patch);
|
||||
}
|
||||
@@ -2333,7 +2362,13 @@ function bootData(){
|
||||
if(ix>=0) wpNavOpen(ix);
|
||||
else toast('That work package is not on this project (it may have been deleted).');
|
||||
}
|
||||
if(p.get('view')==='dashboard' || location.hash==='#dashboard'){ showDashboard(); }
|
||||
// A pipeline cell links straight to a filtered board, so the filter has to be
|
||||
// applied BEFORE the first render — applying it after would paint the whole
|
||||
// board and then throw it away, which on a big project is visible.
|
||||
if(p.get('view')==='dashboard' || location.hash==='#dashboard'){
|
||||
dashApplyFlag(p.get('flag') || '');
|
||||
showDashboard({fromUrl:true});
|
||||
}
|
||||
|
||||
// Back / Forward. The URL is the state, so restoring is "read it and show that",
|
||||
// not a bespoke undo stack. Guarded against re-pushing while we restore, or every
|
||||
@@ -2342,6 +2377,10 @@ function bootData(){
|
||||
WPUrl.onChange(function(state, viaPop){
|
||||
if(!viaPop) return;
|
||||
if(state.view === 'dashboard'){
|
||||
// The flag first: showDashboardFromUrl() renders, so setting it after
|
||||
// would paint the unfiltered board and then replace it. dashApplyFlag
|
||||
// re-renders itself only when the board is already open.
|
||||
dashApplyFlag(state.flag || '');
|
||||
if(currentView !== 'Dashboard') showDashboardFromUrl();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user