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:
108
html/index.html
108
html/index.html
@@ -317,6 +317,39 @@
|
||||
.field-error { color: var(--cds-text-error); font-size: 12px; font-weight: 600; margin-top: 0.3rem; }
|
||||
.field-error:empty { display: none; }
|
||||
|
||||
/* PIPELINE STRIP — B4 surface (T5.3)
|
||||
Four counts, every one of them from /api/wps/metrics. There is deliberately
|
||||
no localStorage fallback anywhere in here: a per-browser number that looks
|
||||
authoritative is the thing B4 removes, and a stale four beside a live four
|
||||
is worse than an error. */
|
||||
.pipeline { margin-bottom: 1.5rem; }
|
||||
.pipeline-head { font-size: 13px; font-weight: 600; text-transform: none;
|
||||
color: var(--cds-text-secondary); margin-bottom: 0.6rem; }
|
||||
.pipeline-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1px;
|
||||
background: var(--cds-border-subtle); border: 1px solid var(--cds-border-subtle); }
|
||||
.pipe-cell { display: flex; flex-direction: column; gap: 0.15rem; padding: 1rem 1.1rem;
|
||||
background: var(--cds-layer); text-decoration: none; color: var(--cds-text-primary);
|
||||
border-left: 3px solid transparent; transition: background 0.15s; }
|
||||
.pipe-cell:hover { background: var(--cds-layer-hover); }
|
||||
.pipe-num { font-size: 1.75rem; font-weight: 300; line-height: 1.1; }
|
||||
.pipe-label { font-size: 13px; font-weight: 600; }
|
||||
.pipe-sub { font-size: 12px; color: var(--cds-text-secondary); }
|
||||
/* Each cell's accent says which slice it is, and the label says it in words —
|
||||
the number alone is the same shape in all four (C1). */
|
||||
.pipe-cell.is-total { border-left-color: var(--cds-interactive-01); }
|
||||
.pipe-cell.is-ready { border-left-color: var(--cds-support-success); }
|
||||
.pipe-cell.is-onhold { border-left-color: var(--wp-status-warning-text); }
|
||||
.pipe-cell.is-overdue { border-left-color: var(--cds-support-error); }
|
||||
/* Zero is a real answer for one slice of a project that has work in it. Four
|
||||
zeros on a project with no work packages at all is not a reading, it is a
|
||||
broken-looking strip, so that case gets a sentence instead. */
|
||||
.pipe-empty, .pipe-error { grid-column: 1 / -1; background: var(--cds-layer);
|
||||
padding: 1.1rem 1.2rem; font-size: 13px; color: var(--cds-text-secondary); }
|
||||
.pipe-error { color: var(--wp-status-warning-text); }
|
||||
.pipe-empty a { color: var(--cds-link-primary); }
|
||||
.pipe-loading { grid-column: 1 / -1; background: var(--cds-layer); padding: 1.1rem 1.2rem;
|
||||
font-size: 13px; color: var(--cds-text-secondary); font-style: italic; }
|
||||
|
||||
/* FIRST RUN
|
||||
A brand-new account has no projects, so it has no tool cards either — this
|
||||
is the whole page for that person, and it has to say what to do next. */
|
||||
@@ -437,6 +470,16 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PIPELINE STRIP — B4 surface / T5.3
|
||||
Four counts from /api/wps/metrics, each one a link into the dashboard
|
||||
filtered to that slice. The URL carries the filter (?view=dashboard&
|
||||
flag=…), so a cell can be sent to someone rather than only clicked.
|
||||
aria-live because it refreshes in place when the project changes. -->
|
||||
<section class="pipeline" id="pipeline" hidden aria-labelledby="pipeline-head">
|
||||
<h2 class="pipeline-head" id="pipeline-head">Work package pipeline</h2>
|
||||
<div class="pipeline-strip" id="pipeline-strip" aria-live="polite" aria-busy="true"></div>
|
||||
</section>
|
||||
|
||||
<!-- TOOL CARDS (shown once a project is active) -->
|
||||
<div class="cards-grid" id="overview" style="display:none">
|
||||
|
||||
@@ -712,6 +755,8 @@
|
||||
|
||||
if(!active){
|
||||
cards.style.display = 'none';
|
||||
const strip = document.getElementById('pipeline');
|
||||
if(strip) strip.hidden = true;
|
||||
heroTitle.textContent = 'Work Package Suite';
|
||||
// Two different situations, and telling someone to "select a project"
|
||||
// when there are none to select is the thing B3 is about.
|
||||
@@ -737,6 +782,69 @@
|
||||
// reflectSOPStatus.
|
||||
if(ProjectData.pullProject){ ProjectData.pullProject(active.id).catch(()=>{}); }
|
||||
reflectSOPStatus(active);
|
||||
renderPipeline(active);
|
||||
}
|
||||
|
||||
// ── PIPELINE STRIP (B4 surface / T5.3) ────────────────────────────────────
|
||||
// Four counts, all four from /api/wps/metrics. There is deliberately no cache
|
||||
// fallback: B4's whole point is that a per-browser number which looks
|
||||
// authoritative is worse than no number, and the same is true of a remembered
|
||||
// one shown next to live ones.
|
||||
//
|
||||
// The flags match wp-creation-app.js's DASH_FLAGS. A cell linking to a filter
|
||||
// the dashboard does not recognise is a dead link that still looks live.
|
||||
const PIPE_CELLS = [
|
||||
{flag: '', cls: 'is-total', label: 'Work packages', key: 'total',
|
||||
sub: 'everything on this project'},
|
||||
{flag: 'ready', cls: 'is-ready', label: 'Release ready', key: 'release_ready',
|
||||
sub: 'no open constraints, nothing waiting'},
|
||||
{flag: 'onhold', cls: 'is-onhold', label: 'On hold', key: 'on_hold',
|
||||
sub: 'raised as an issue'},
|
||||
{flag: 'overdue', cls: 'is-overdue', label: 'Overdue', key: 'overdue',
|
||||
sub: 'past due and not closed'},
|
||||
];
|
||||
|
||||
function dashHref(projectId, flag){
|
||||
const q = new URLSearchParams({ view: 'dashboard', project: projectId });
|
||||
if(flag) q.set('flag', flag);
|
||||
return 'work-package-suite.html?' + q.toString();
|
||||
}
|
||||
|
||||
function renderPipeline(active){
|
||||
const host = document.getElementById('pipeline');
|
||||
const strip = document.getElementById('pipeline-strip');
|
||||
if(!host || !strip) return;
|
||||
host.hidden = false;
|
||||
strip.setAttribute('aria-busy', 'true');
|
||||
strip.innerHTML = '<div class="pipe-loading">Counting work packages…</div>';
|
||||
|
||||
fetch('/api/wps/metrics?project_id=' + encodeURIComponent(active.id),
|
||||
{ headers: { 'Accept': 'application/json' } })
|
||||
.then(r => { if(!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
||||
.then(m => {
|
||||
if(ProjectData.getActiveId() !== active.id) return; // switched while in flight
|
||||
strip.setAttribute('aria-busy', 'false');
|
||||
if(!m.total){
|
||||
// Four zeros on a project with no work packages is not a reading, it
|
||||
// is a strip that looks broken. Say the true thing instead.
|
||||
strip.innerHTML = `<div class="pipe-empty">No work packages on this project yet.
|
||||
<a href="${esc(dashHref(active.id, ''))}">Open the creator</a> to write the first one.</div>`;
|
||||
return;
|
||||
}
|
||||
strip.innerHTML = PIPE_CELLS.map(c => `
|
||||
<a class="pipe-cell ${c.cls}" href="${esc(dashHref(active.id, c.flag))}">
|
||||
<span class="pipe-num">${Number(m[c.key] || 0)}</span>
|
||||
<span class="pipe-label">${esc(c.label)}</span>
|
||||
<span class="pipe-sub">${esc(c.sub)}</span>
|
||||
</a>`).join('');
|
||||
})
|
||||
.catch(err => {
|
||||
if(ProjectData.getActiveId() !== active.id) return;
|
||||
strip.setAttribute('aria-busy', 'false');
|
||||
strip.innerHTML = `<div class="pipe-error">⚠ Could not load work package counts —
|
||||
${esc((err && err.message) || 'offline')}. These numbers come from the server;
|
||||
nothing stale is shown in their place.</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function clearActiveProject(){ ProjectData.setActive(null); urlSyncProject('', false); renderProjectEntry(); applyActiveProject(); }
|
||||
|
||||
Reference in New Issue
Block a user