diff --git a/packages/webui/src/public/workspace.js b/packages/webui/src/public/workspace.js index 77d42dc89..c15392a14 100644 --- a/packages/webui/src/public/workspace.js +++ b/packages/webui/src/public/workspace.js @@ -156,7 +156,7 @@ const titleCase = s => String(s || '').replace(/(^|[-_\s])(\w)/g, (_, sep, c) => `${sep ? ' ' : ''}${c.toUpperCase()}`); const clock = () => new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`; - const secH = (title, note = '', link = '') => `

${title}

${note ? `${note}` : ''}${link}
`; + const secH = (title, note = '', link = '', attrs = '') => `
${title}${note ? `${note}` : ''}${link}
`; function settingsList(org, ctx) { const { business, settings, instances, orgCount } = ctx; @@ -256,15 +256,17 @@ if (!org) return `

Organization unavailable

${list ? 'This organization is not on your list.' : 'The organization list could not be read.'}

`; // E2E6 P2-8: a stopped bus is not read for the bus-backed editors; each says the bus is stopped. const stopped = !org.busHealthy && org.busStopped === true; + // SP8 (#1873): the cross-organization read scoped to this one gives the tiles' figures and, SP1, + // while the bus is down, the reason the banner above everything else says. It runs beside the + // organization's own reads, not after them. + const wsRead = data.workspace({ org: id, sections: ['sessions', 'projects', 'metrics'] }).catch(() => null); const reads = await Promise.allSettled([data.business(id), data.settings(id), data.instances(id), data.threads(id), data.projects(id), data.queue(id), data.agents(id), data.claims(id), ...(stopped ? [] : [window.MosaicAutonomy?.load(id), window.MosaicPacks?.load(id), window.MosaicWorkflows?.load(id)])]); const value = i => (reads[i].status === 'fulfilled' ? reads[i].value : null); const business = value(0), settings = value(1), instances = instancesOf(value(2)); const threads = rowsOf(value(3)), projectRows = rowsOf(value(4)), queue = rowsOf(value(5)); const agents = rowsOf(value(6)), claims = rowsOf(value(7)); const docRead = business?.business ? await docs(id, business.business.projects).catch(() => ({ rows: [], failed: true })) : null; - // SP8 (#1873): the cross-organization read scoped to this one gives the tiles' figures and, SP1, - // while the bus is down, the reason the banner above everything else says. - const ws = await data.workspace({ org: id, sections: ['sessions', 'projects', 'metrics'] }).catch(() => null); + const ws = await wsRead; const wsRow = arr(ws?.orgs).find(r => r?.org === id) ?? null; const downRow = org.busHealthy ? null : wsRow; await window.MosaicConsole?.ready; @@ -653,6 +655,8 @@ const parts = (key, f) => { const ok = readers(o, SRC[key]); return per.filter(x => ok.has(x.org)).map(x => ({ id: x.org, n: num(f(x)) })); }; const sum = ps => ps.reduce((a, p) => a + p.n, 0); const fig = (key, ps) => (m && readers(o, SRC[key]).size ? sum(ps) : null); + // A response with no metrics section leaves every figure tile quiet; it says so rather than a bare "Not read". + const noFig = m ? {} : { none: 'Not read: no figures were read' }; const answered = o.rows.filter(r => r.state === 'ok').length; // Agents working of all agents: the system Agents page's rows, read the way that page reads them. // An organization whose agent read failed is named, never counted as having none. @@ -666,10 +670,10 @@ const tiles = !o.rows.length ? '

No organizations yet, so there is nothing to count.

' : `
${kpiTile(o, { key: 'agents', name: 'Agents working', href: '#/agents?state=working', big: agentOk.length ? sum(agentParts) : null, small: `of ${agentAll.length}`, parts: agentParts, delta: waitingOn ? `${waitingOn} waiting on you` : '', miss: [...notIn(['agents']), ...agentReads.filter(r => !r.inst).map(r => `${esc(o.name(r.id))} (${esc(instWhy(r))})`)] })} ${kpiTile(o, { key: 'orgs', name: 'Organizations', href: '#/dashboard', big: answered, small: `of ${o.rows.length} running`, delta: o.unread.length ? andList(o.unread.map(r => `${esc(o.name(r.org))} ${offWord(r)}`)) : 'every bus answered', attrs: ' data-ws-jump="orgs"' })} - ${kpiTile(o, { key: 'needs', name: 'Needs you', href: '#/inbox', big: needsN, small: needsN === 1 ? 'item' : 'items', parts: ps.needs, delta: needsN && t?.oldestNeedsYouAt ? `oldest ${span(t.oldestNeedsYouAt)}` : 'nothing waiting', miss: notIn(SRC.needs) })} - ${kpiTile(o, { key: 'tasks', name: 'Open tasks', href: '#/tasks', big: fig('tasks', ps.tasks), parts: ps.tasks, miss: notIn(SRC.tasks) })} - ${kpiTile(o, { key: 'done', name: 'Done this week', href: '#/tasks', big: fig('done', ps.done), parts: ps.done, miss: notIn(SRC.done) })} - ${kpiTile(o, { key: 'projects', name: 'Projects', href: selHref('/projects'), big: fig('projects', ps.projects), small: 'active', parts: ps.projects, delta: `${archived} archived`, miss: notIn(SRC.projects) })} + ${kpiTile(o, { key: 'needs', name: 'Needs you', href: '#/inbox', big: needsN, small: needsN === 1 ? 'item' : 'items', parts: ps.needs, delta: needsN && t?.oldestNeedsYouAt ? `oldest ${span(t.oldestNeedsYouAt)}` : 'nothing waiting', miss: notIn(SRC.needs), ...noFig })} + ${kpiTile(o, { key: 'tasks', name: 'Open tasks', href: '#/tasks', big: fig('tasks', ps.tasks), parts: ps.tasks, miss: notIn(SRC.tasks), ...noFig })} + ${kpiTile(o, { key: 'done', name: 'Done this week', href: '#/tasks', big: fig('done', ps.done), parts: ps.done, miss: notIn(SRC.done), ...noFig })} + ${kpiTile(o, { key: 'projects', name: 'Projects', href: selHref('/projects'), big: fig('projects', ps.projects), small: 'active', parts: ps.projects, delta: `${archived} archived`, miss: notIn(SRC.projects), ...noFig })}
`; // SP1 (sample r2-01): one row per organization. A figure shows only when its read gave it; an @@ -726,7 +730,7 @@ ${tiles}
- ${secH('Organizations', `read ${esc(hm(body.readAt))}`).replace('

', '

')} + ${secH('Organizations', `read ${esc(hm(body.readAt))}`, '', 'tabindex="-1" data-ws-orgs-h data-focus-key="orgs-h"')} ${orgList} ${secH('Recent project activity', 'latest change per project; there is no activity feed yet', `All projects`)} ${trail}${missLine(o, ['projects'], 'recent')}