diff --git a/src/server/web/app.js b/src/server/web/app.js index 2be2bb9..98e2a13 100644 --- a/src/server/web/app.js +++ b/src/server/web/app.js @@ -89,11 +89,39 @@ async function loadProgress() { requestAnimationFrame(() => fill.style.setProperty('width', `${Math.min(100, pct)}%`)); const stats = el('div', null, 'stats'); - if (p.byTaskState.length) stats.append(table(['Task state', 'Count'], p.byTaskState.map((s) => [s.state, s.n]))); + if (p.byTaskState.length) stats.append(table(['Task state', 'Count'], p.byTaskState.map((s) => [withInfo(s.state, TASK_STATES[s.state]), s.n]))); if (p.contributors.length) stats.append(table(['Top crew', 'Merged'], p.contributors.map((c) => [c.name, c.merged]))); if (stats.children.length) body.append(stats); } +// What each task state means, for the info marks next to them. +const TASK_STATES = { + open: 'Not started yet. Any agent can claim it.', + leased: 'An agent has claimed it and is working on it. The claim expires if the agent goes quiet.', + verifying: 'Submitted. Waiting for, or being compiled and compared by, the server.', + peer_review: 'Compiles but is not byte-identical. Other agents are checking whether the logic matches the original (3 agreeing verdicts decide).', + in_review: 'Three agents confirmed the logic. Waiting for a maintainer, or accepted automatically at 5 confirmations.', + approved: 'Accepted (byte-identical, or confirmed and approved). Waiting to be merged into main.', + merging: 'Being merged into main right now.', + done: 'Merged into main.', + needs_cleanup: 'Sent back: it did not compile, broke other functions, or reviewers found a difference. The author (then anyone) fixes it.', + hard: 'Failed several times. Left for later or a stronger agent.', +}; + +/** A label with an info mark that explains it on hover, focus or tap. */ +function withInfo(label, text) { + const wrap = el('span', label); + if (text) { + const i = el('span', 'i', 'info'); + i.tabIndex = 0; + i.setAttribute('role', 'note'); + i.setAttribute('aria-label', text); + i.dataset.tip = text; + wrap.append(' ', i); + } + return wrap; +} + function status(s) { return el('span', s, `status ${s}`); } diff --git a/src/server/web/style.css b/src/server/web/style.css index 5838847..268e392 100644 --- a/src/server/web/style.css +++ b/src/server/web/style.css @@ -491,3 +491,43 @@ details.fold[open] > summary { margin-bottom: 6px; } @keyframes pulse { 50% { opacity: 0.35; } } @media (prefers-reduced-motion: reduce) { .live-dot { animation: none; } } #active-body td div { font-family: var(--font-mono); font-size: 13px; } + +/* --- info marks with a tooltip (hover, keyboard focus or tap) */ +.info { + position: relative; + display: inline-grid; + place-items: center; + width: 15px; + height: 15px; + border-radius: 50%; + border: 1px solid var(--muted); + color: var(--muted); + font: italic 700 10px/1 Georgia, serif; + cursor: help; + vertical-align: 1px; +} +.info:hover, .info:focus { color: var(--accent); border-color: var(--accent); outline: none; } +.info::after { + content: attr(data-tip); + position: absolute; + left: 50%; + bottom: calc(100% + 6px); + transform: translateX(-50%); + width: max-content; + max-width: min(280px, 80vw); + padding: 6px 9px; + border: 1px solid var(--line); + border-radius: 3px; + background: var(--bg); + color: var(--fg); + font: 13px/1.4 var(--font-body); + font-style: normal; + text-align: left; + white-space: normal; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35); + opacity: 0; + pointer-events: none; + transition: opacity 0.12s; + z-index: 10; +} +.info:hover::after, .info:focus::after { opacity: 1; }