surfaces.ts
Language TypeScript71.4 KB1572 lines
/*** The six surfaces from the original dashboard mockups, built from real data.** board 1a Bird's-eye. Headline numbers that drill down.* briefingProse 1b The morning briefing as prose. Reads like a person wrote it.* briefingData 1c The same facts, scannable in fifteen seconds.* myDay 1d One person: what is mine, what I promised.* elsewhere 1e What moved outside your lane.* dataHealth 1f What the board can and cannot see.** 1g, the phone layout, is not a seventh surface. It is the same markup under* the media queries in `theme.ts`. The mockup shows a phone because that is* what most people will actually open before the morning meeting, not because it is* a different page. A separate page would drift from this one.*/import { CLASS_LABELS } from '@meridian/plinth-client'import { MERIDIAN_TZ, calendarDate, localClock, localDay, localStamp, shortDate } from '@meridian/ninety-client'import { esc, join, map, orUntracked, plural } from './html.js'import { UNCATEGORIZED, bucketByTopic, type TopicBucket } from '../../../config/fathom-topics.js'import { onboardingFor, type OnboardingPlan } from '../../../config/onboarding.js'import { checkinPlaceholder } from './checkin-view.js'import { ownListPlaceholder } from './own-list-view.js'import { hasCheckin } from '../../../config/checkin.js'import { hasOwnList } from '../../../config/own-list.js'import { C, UNTRACKED_FILL } from './theme.js'import type { Briefings } from './briefings.js'import { COMPANY_VIEWER, companyBriefingKey, type Viewer } from './viewer.js'import type { Snapshot } from './collect.js'import {COVERAGE_COLUMNS,toneColor,type Drill,type MyDay,type PersonView,type QueueGroupView,type QueueRowView,type ViewModel,} from './view-model.js'const bar = (pct: string, fill: string, h = 16): string =>`<div class="bar-track" style="height:${h}px"><div class="bar-fill" style="width:${esc(pct)};background:${fill}"></div></div>`// --- 1a: the bird's-eye board -------------------------------------------------/*** A tile whose source is down shows an em-dash, not a zero.** This is the single rule the whole page is built on and the tile is where it is* easiest to break: a confident `0` in 38px type reads as "nothing to do here"* even with a failure banner directly above it. Someone scanning the board on* their phone at 9:12 sees the number, not the banner.*/function tile(opts: {key?: stringlabel: stringvalue: numbernote: stringmeta?: stringdelta?: stringchip?: stringvariant?: 'hero' | 'accent' | ''color?: stringavailable?: boolean}): string {const { key, label, value, note, meta, delta, chip, variant = '', color, available = true } = optsconst inner = join([`<div class="tile-body">`,`<div class="tile-label">${esc(label)}</div>`,`<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:10px">`,available? `<div class="tile-value"${color ? ` style="color:${color}"` : ''}>${value}</div>`: `<div class="tile-value" style="color:var(--meridian-gray-400)" title="the source this counts is not answering">–</div>`,delta && available ? `<span class="delta">${esc(delta)}</span>` : '',chip && available ? chip : '',`</div>`,`<div class="tile-note">${available ? esc(note) : 'source unavailable, not zero'}</div>`,meta && available ? `<div class="tile-note" style="margin-top:4px">${meta}</div>` : '',`</div>`,])// The affordance row makes a drill visible before it is clicked. A context// tile gets no rule and no footer; that absence is the signal.const foot = key && available? `<span class="tile-foot"><span>Open the ${value}</span><span aria-hidden="true">→</span></span>`: ''if (!key || !available) return `<div class="tile ${variant}"><div class="rule"></div>${inner}</div>`return `<button type="button" class="tile ${variant}" data-drill="${esc(key)}"aria-expanded="false" aria-controls="drill"><div class="rule"></div>${inner}${foot}</button>`}function drillPanel(drills: Record<string, Drill>): string {const hue = (key: string): string =>key === 'replied' ? C.magenta : key === 'design' ? C.blue : key === 'exceptions' ? C.orange : C.blueconst panel = (d: Drill): string => {const count = d.rows.length + d.morereturn `<div class="drill" data-drill-panel="${esc(d.key)}" hidden><div class="drill-head"><span class="n">${count}</span><span class="t">${esc(d.title.split('·')[0]?.trim() ?? d.title)}</span><span class="m">${esc(d.note)}</span><button type="button" class="btn btn-secondary" data-drill-close>Close</button></div><div class="drill-body">${d.rows.length? map(d.rows,(r) => `<div class="drill-row"><div class="bar" style="background:${hue(d.key)}"></div><div class="id">${esc(r.id)}</div><div style="line-height:1.45">${esc(r.what)}</div><div class="who">${esc(r.who)}</div><div class="age">${esc(r.age)}</div></div>`,): '<p class="empty" style="padding:14px 18px">Nothing in this bucket right now.</p>'}${// more not shown, on purpose, and the arithmetic is stated: rows shown// plus withheld equals the tile, so the number and the list reconcile.d.more? `<div class="withheld"><span class="n">${d.more}</span><span><b>more not shown, on purpose.</b> This panel lists the ${d.rows.length} oldest so itstays readable on a phone. ${d.rows.length} + ${d.more} = ${count}, which is the tile above:the number and the list always reconcile.</span></div>`: ''}</div></div>`}return `<div id="drill">${map(Object.values(drills), panel)}</div>`}/*** The bar is a measurement: the field is fixed (25 items = 100%, a tick every* 5), so the same workload draws the same bar every day, rather than a* percentage of whoever happens to be busiest. The fill is the source mix in* legend order, so a bar answers "how much, of what" at a glance.** Past `border-left: 2px solid` counting stops: waiting-on-client is drawn as* magenta ghost hatch: drawn because it exists, hatched because it is the* client's clock. The not-tracked hatch runs to the edge of the field, never to* a value, because absence has no magnitude.*/const FIELD = 25function measureBar(p: PersonView): string {const w = (n: number | null | undefined): number => Math.min(100, ((n ?? 0) / FIELD) * 100)const segs = [{ n: p.plinthOurMove, hue: C.blue, t: 'order lines' },{ n: p.taskCount, hue: C.orange, t: 'Bitrix tasks' },{ n: p.todos, hue: C.blueDeep, t: 'Ninety to-dos' },{ n: p.sheetOurMove, hue: C.yellow, t: 'tracker items' },].filter((x) => (x.n ?? 0) > 0)return `<div class="measure">${map(segs, (x) => `<span class="seg" style="width:${w(x.n)}%;background:${x.hue}" title="${x.n} ${esc(x.t)}"></span>`)}${p.plinthWaiting ? `<span class="ghost" style="width:${w(p.plinthWaiting)}%" title="${p.plinthWaiting} waiting on client; listed, never counted"></span>` : ''}${p.plinthOurMove === null? `<span class="rest" title="no source covers this person's orders"><span class="lbl"><span>orders not tracked</span></span></span>`: ''}</div>`}const initials = (name: string): string =>name.split(/\s+/).map((w) => w[0] ?? '').join('').slice(0, 2).toUpperCase()function personRow(p: PersonView): string {return `<div class="person"><div class="person-id"><span class="avatar" aria-hidden="true">${esc(initials(p.name))}</span><span><span class="person-name">${esc(p.name)}</span><br><span class="person-role">${esc(p.role)}</span></span></div><div class="person-bar"><div class="ruler" aria-hidden="true"><span>0</span><span>5</span><span>10</span><span>15</span><span>20</span><span>25</span></div>${measureBar(p)}<div class="person-detail">${esc(p.detail)}${p.aside ? ` <span class="person-aside">· ${esc(p.aside)}</span>` : ''}</div>${p.coverageNote ? `<div class="person-note">${esc(p.coverageNote)}</div>` : ''}</div><div class="person-load"><div class="v">${orUntracked(p.load)}</div><div class="k">${esc(p.loadNote)}</div>${p.plinthOurMove === null && p.load !== null ? `<div style="margin-top:4px"><span class="untracked">+ orders</span></div>` : ''}</div></div>`}export function board(vm: ViewModel, afterHeadline = ''): string {const h = vm.headlineconst orders = vm.snap.orderLines.okconst leads = vm.snap.leads.okconst exceptionsCount = vm.drills.exceptions ? vm.drills.exceptions.rows.length + vm.drills.exceptions.more : 0const d0 = vm.briefing.headlineDeltas[0]?.deltaconst d1 = vm.briefing.headlineDeltas[1]?.deltaconst deltaText = (d: { value: number; direction: string; since: string } | null | undefined): string | undefined =>d && d.direction !== 'flat'? `${d.direction === 'up' ? '↑' : '↓'} ${Math.abs(d.value)} since ${d.since}`: undefinedreturn join([`<section class="section"><div class="section-head"><h2>Headline</h2><span class="hint">Only “our move” is workload. Everything else is context.</span></div><div class="tiles">${tile({ key: 'ours', label: 'Our move', value: h.ourMove, note: 'orders + leads where Meridian owes the next step', meta: vm.drills.ours ? esc(vm.drills.ours.note.split('.')[0] ?? '') : undefined, delta: deltaText(d0), variant: 'hero', available: orders && leads })}${tile({ key: 'design', label: 'Design queue', value: h.designQueue, note: 'in artwork, the closest proxy we have for open mockups, and a proxy is what it is', delta: deltaText(d1), color: C.blue, available: orders })}${tile({ key: 'replied', label: 'Client replied', value: h.needsReply, note: 'they are waiting on us, nobody else. Answer these before anything below', chip: '<span class="answer-chip">Answer first</span>', variant: 'accent', color: C.magenta, available: leads })}${tile({key: 'exceptions',label: 'Exceptions',value: exceptionsCount,note:exceptionsCount === 0? 'payment and cancellation cases needing a human. Plinth answered and the bucket is empty, so this is free capacity, not a gap.': 'payment and cancellation cases needing a human; each needs a decision, not a task',chip: exceptionsCount === 0 && orders ? '<span class="zero-chip">A real zero</span>' : undefined,color: exceptionsCount ? C.orange : undefined,available: orders,})}</div><div class="ctx"><div class="ctx-key">Context,<br>not workload</div><div class="ctx-cell"><span class="n">${orders && leads ? h.waitingOnClient : '–'}</span><span><span class="t">Waiting on client</span><div class="s">Follow-up list. It is the client’s clock, not the team’s bandwidth, so it is listed and never counted.</div></span></div><div class="ctx-cell"><span class="n">${orders ? h.atVendor : '–'}</span><span><span class="t">At vendor / in transit</span><div class="s">Delivery risk, not capacity. Counting it as backlog is how a board makes the team look behind when it is not.</div></span></div></div>${drillPanel(vm.drills)}</section>`,// Injected between the numbers and the workload: the inline briefing and// the legend live here. The numbers lead, prose second, and the legend sits// directly above the first bars it explains.afterHeadline,`<section class="section"><div class="section-head" style="justify-content:space-between;width:100%"><h2>Workload by person</h2><div class="section-head" style="margin:0;gap:8px"><span class="hint">Bar counts:</span><button type="button" class="btn btn-outline" data-basis="ours" aria-pressed="true">our move only</button><button type="button" class="btn btn-outline" data-basis="all" aria-pressed="false">+ waiting on client</button></div></div><p class="note wide" style="margin-bottom:14px">A zero means free capacity. An untracked cell does not. Anyone no source can see says soinstead of showing a number.</p><div class="card card-pad"><div data-basis-panel="ours">${map(vm.people, personRow)}</div><div data-basis-panel="all" hidden>${map(vm.peopleAll, personRow)}</div><div class="legend" style="padding-top:12px"><span>Waiting-on-client is listed, never counted. It is the client’s clock, not their bandwidth.</span>${vm.board.offRosterAccounts.length ? `<span>Work also sits with <b>${esc(vm.board.offRosterAccounts.join(', '))}</b>, not on the roster.</span>` : ''}${vm.board.junkTasks ? `<span>${vm.board.junkTasks} Bitrix ${plural(vm.board.junkTasks, 'task')} excluded as automation noise.</span>` : ''}</div></div></section>`,`<section class="section"><div class="two-up" style="display:grid;grid-template-columns:1fr 1fr;gap:20px"><div><div class="section-head"><h2>Orders by status</h2><span class="hint">every open product line, grouped by where it sits in production</span></div><div class="card card-pad"><div class="legend" style="margin-bottom:14px"><span><i class="swatch" style="background:${C.blue}"></i>${esc(CLASS_LABELS.ours)}</span><span><i class="swatch" style="background:${C.magenta}"></i>${esc(CLASS_LABELS.client)}</span><span><i class="swatch" style="background:${C.green}"></i>${esc(CLASS_LABELS.vendor)}</span><span><i class="swatch" style="background:${C.gray}"></i>Closed / parked</span></div>${vm.orderStages.length? map(vm.orderStages,(s) => `<div class="stage-row"><div class="n" title="${esc(s.name)}">${esc(s.name)}</div>${bar(s.pct, s.color)}<div class="c">${s.count}</div></div>`,): `<p class="empty">${vm.snap.orderLines.ok ? 'No order lines in the window.' : 'Plinth did not answer. This is missing, not zero.'}</p>`}</div></div><div><div class="section-head"><h2>Oldest open leads</h2><span class="hint">the leads that have waited longest for anyone’s next move</span></div><div class="card card-pad"><div class="row-head lead-cols"><div>Age</div><div class="stage">Stage</div><div>Lead</div></div>${vm.leads.length? map(vm.leads,(l) => `<div class="row lead-cols"><div class="num" style="color:${l.ageColor};font-weight:600">${l.age}d</div><div class="stage" style="font-size:12px;color:var(--text-muted)">${esc(l.stage)}</div><div title="${esc(l.company ?? '')}">${esc(l.title)}</div></div>`,): `<p class="empty">${vm.snap.leads.ok ? 'No dated open leads.' : 'Bitrix did not answer. This is missing, not zero.'}</p>`}<p class="note" style="margin-top:12px">Parked leads are excluded: they are paused on purpose, and including them buries theones that are actually aging.</p></div></div></div></section>`,])}// --- 1b: the briefing, prose --------------------------------------------------export function briefingProse(vm: ViewModel): string {const b = vm.briefingconst d = b.headlineDeltas[1]?.deltaconst growth =d === null || d === undefined? 'There is no earlier run to compare it against yet.': d.direction === 'up'? `That queue grew by ${d.value} since ${esc(d.since)} and is the biggest single thing on the board.`: d.direction === 'down'? `That queue is down ${Math.abs(d.value)} since ${esc(d.since)}.`: `That queue has not moved since ${esc(d.since)}.`const replied = vm.drills.replied?.rows ?? []const exceptions = vm.drills.exceptions?.rows ?? []return `<div class="card"><div class="rule"></div><div style="padding:32px 40px 12px"><div class="eyebrow">Morning Briefing</div><h1 class="masthead" style="font-family:var(--font-display);font-size:30px;padding:0">${esc(b.dateLong)}</h1><p class="note" style="margin-top:8px">Written at ${esc(b.generatedAt)} CT from that sync. Read before the morning meeting.</p></div><div class="brief" style="padding:14px 40px 0"><p class="kicker">${// "0 things need a move from Meridian today" is the most damaging sentence// this page could produce, and an outage is exactly when it would write// it. When the sources behind the count are down, the briefing leads// with that instead of with a number.vm.briefing.headlineDeltas[0]?.available? `<b>${vm.headline.ourMove} things need a move from Meridian today</b>, and ${vm.headline.designQueue}of them are sitting in the design queue. ${growth}`: `<b>This morning’s counts are unavailable.</b> ${esc(vm.failures.join(' '))}Nothing below is a zero. It is a gap, and the board should not be read as“quiet” until the source is back.`}</p><div style="height:1px;background:var(--border-default);margin:0 0 20px"></div><h3>Answer first</h3><p>${replied.length? `${replied.length} ${plural(replied.length, 'lead')} moved to <b>Client Replied</b>: ${esc(replied.slice(0, 2).map((r) => r.subject).join('; '))}. They are waiting on us, nobody else is.`: 'No lead is sitting in Client Replied this morning; nothing is waiting on a reply from us.'}${exceptions.length? ` ${exceptions.length} ${plural(exceptions.length, 'exception')} ${plural(exceptions.length, 'needs', 'need')} a decision rather than a task${exceptions.filter((e) => e.who === 'unassigned').length? `, and ${exceptions.filter((e) => e.who === 'unassigned').length} of those have no owner`: ''}.`: ''}</p><h3>Who is loaded</h3><p>${map(b.loadedSentences, (s, i) => (i === b.loadedSentences.length - 1 && vm.people.some((p) => p.plinthOurMove === null) ? `<span class="quiet">${esc(s)}</span>` : `${esc(s)} `))}</p><div class="blind"><h3>What I could not see this morning</h3><p>${b.blindSpots.length ? esc(b.blindSpots.join(' ')) : 'Every source answered and nothing is stale.'}</p></div><p class="note" style="margin-bottom:6px">An open to-do does not mean unfinished work: items are only closed at Monday reviews, so“late” here counts review gates missed, not calendar days.</p></div><div class="rule" style="margin-top:20px"></div><div style="background:var(--meridian-blue-050);padding:14px 40px;font-size:12px;display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap"><span>Generated by the briefing job · <a href="#" data-goto="board">see the full board</a></span><span class="note">${b.comparedTo ? `Compared with ${esc(b.comparedTo)}` : 'First run, no comparison yet'}</span></div></div>`}// --- 1c: the briefing, data-led ------------------------------------------------function deltaTag(d: { value: number; direction: string; since: string } | null, goodWhenDown = true): string {if (!d) return `<div class="note" style="font-size:11px">no prior run</div>`if (d.direction === 'flat') return `<div class="delta flat" style="font-size:11px">no change</div>`const good = goodWhenDown ? d.direction === 'down' : d.direction === 'up'const arrow = d.direction === 'up' ? '▲' : '▼'return `<div class="delta ${good ? 'down' : 'up'}" style="font-size:11px" title="since ${esc(d.since)}">${arrow} ${Math.abs(d.value)} vs ${esc(d.since)}</div>`}/*** 1c. The same facts, scannable.** This surface is company-visible, so it carries no meeting content: no* unowned commitments, no meeting titles. Unowned items reach the participants* of their own meeting through myDay(), and nobody else.** Explanations like this one belong in TypeScript comments, never in the* markup: an HTML comment ships to every reader, and a `//` inside a template* literal is not a comment at all and prints onto the page.* fathom-gating.test.ts asserts against the rendered document for exactly that* reason.*/export function briefingData(vm: ViewModel): string {const b = vm.briefingreturn `<div class="card"><div class="rule"></div><div class="top" style="display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:22px 28px 18px;border-bottom:1px solid var(--border-default);flex-wrap:wrap"><div><h1 style="font-family:var(--font-display);font-size:26px;color:var(--text-body)">${esc(b.dateShort)} · Morning Briefing</h1><p class="note" style="margin-top:5px">${esc(b.generatedAt)} CT · ${b.liveSources} of ${b.totalSources} sources live</p></div><div style="display:flex;gap:22px;text-align:right">${map(b.headlineDeltas,(x) => `<div><div class="note" style="font-size:11px">${esc(x.label)}</div>${x.available? `<div style="font-size:26px;font-weight:600;color:${x.color}">${x.value}</div>${deltaTag(x.delta)}`: `<div style="font-size:26px;font-weight:600;color:var(--meridian-gray-400)">–</div><div class="note" style="font-size:11px">source down</div>`}</div>`,)}</div></div><div style="padding:18px 28px 0"><div style="display:flex;align-items:center;gap:8px;margin-bottom:10px"><span class="badge accent">Answer first</span><span class="note">they are waiting on us</span></div>${b.answerFirst.length? map(b.answerFirst,(a) => `<div class="row" style="grid-template-columns:100px 1fr 84px"><div style="font-family:var(--font-mono);font-size:12px;color:var(--meridian-blue)">${esc(a.id)}</div><div>${esc(a.what)}</div><div class="note" style="text-align:right">${esc(a.age)}</div></div>`,): '<p class="empty" style="padding-bottom:10px">Nothing is waiting on a reply from Meridian.</p>'}</div><div style="padding:22px 28px 0"><div style="font-size:13px;font-weight:600;margin-bottom:10px">Board sizes</div>${map(vm.people,(p) => `<div style="display:grid;grid-template-columns:150px 1fr 60px;gap:12px;align-items:center;padding:6px 0"><div style="font-size:13px">${esc(p.short)}</div>${bar(p.pct, p.fill, 12)}<div style="text-align:right;font-size:13px">${orUntracked(p.load)}</div></div>`,)}</div><div style="padding:20px 28px 26px"><div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px">${map(vm.sources,(s) => `<div style="border:1px solid ${toneColor(s.tone)};border-radius:var(--radius-sm);padding:9px 11px"><div style="font-size:11px;font-weight:600">${esc(s.name)}</div><div class="note" style="font-size:11px">${esc(s.status)}</div></div>`,)}</div></div></div>`}// --- orientation ------------------------------------------------------------------/*** What the colors mean, said once, where a first-time reader starts.** Colors on this board are identity, not severity (blue is not calmer than* magenta, it is a different kind of thing). Without a legend a reader has to* induce the scheme from examples, which is exactly the kind of insider* knowledge a board for a small team cannot assume its next new reader has.*/export function legendStrip(): string {const sw = (bg: string) => `<span class="swatch" style="background:${bg}"></span>`return `<div class="card" style="padding:12px 18px;margin-bottom:14px"><details open><summary style="cursor:pointer;font-size:13px;font-weight:600">How to read this board</summary><div class="legend legend-grid" style="margin-top:10px"><span>${sw(C.blue)}our move: work we owe a step on</span><span>${sw(C.magenta)}waiting on client, or past a review gate</span><span>${sw(C.green)}at vendor: theirs, not ours</span><span>${sw(C.orange)}Bitrix task</span><span>${sw(C.blueDeep)}Ninety to-do</span><span>${sw(C.yellow)}Content tracker item</span><span>${sw(UNTRACKED_FILL)}not tracked: no source covers it (never a zero)</span></div><p class="note" style="margin-top:8px">Only “our move” counts as workload. Waiting-on-client and at-vendor are context;counting them would make the team look behind when it is not.</p></details></div>`}// --- 1d: my day -----------------------------------------------------------------/*** Why this list is empty, and the three reasons are not interchangeable.** "Nothing on this board right now" printed over a Ninety 403 is the prime* invariant inverted: a source we cannot see rendered as a desk that is clear. A* person no source covers is a third thing again: a coverage gap somebody should* close, not a quiet morning.*/function emptyBoard(d: MyDay): string {if (d.sourcesDown.length) {const n = d.sourcesDown.lengthreturn (`This list is incomplete, not empty: ${d.sourcesDown.join(' and ')} failed on this ` +`sync, so whatever ${plural(n, 'it holds', 'they hold')} is missing from here rather ` +`than absent.`)}if (d.person.untracked) {return ('No source covers this person, so this list is empty because nothing can see ' +'their work, not because there is none.')}return 'Nothing on this board right now.'}/** Kind metadata: label, hue, and chip text colour (yellow needs dark text). */const KIND = {order: { label: 'Orders', hue: C.blue, on: '#fff' },task: { label: 'Bitrix tasks', hue: C.orange, on: '#0f141d' },todo: { label: 'Ninety to-dos', hue: C.blueDeep, on: '#fff' },tracker: { label: 'Tracker items', hue: C.yellow, on: '#0f141d' },} as const/*** "Do these three first": the whole answer to the five-seconds problem.** The items are already sorted oldest/latest first, so the top three are the* three; this band just stops making the reader derive that from a long list.*/function firstThree(d: MyDay): string {const three = d.items.slice(0, 3)if (!three.length) return ''return `<div style="padding:16px 28px 0"><div class="t-micro" style="color:var(--text-muted);margin-bottom:8px">Do these three first</div><div class="first3">${map(three,(w) => `<div class="f"><div class="strip" style="background:${w.color}"></div><div class="in"><span class="age">${esc(w.age)}</span><span class="kchip" style="background:${KIND[w.kind].hue};color:${KIND[w.kind].on}">${esc(KIND[w.kind].label.replace(/s$/, ''))}</span>${w.id ? `<div class="id">${esc(w.id)}</div>` : '<div style="height:8px"></div>'}<div class="what">${esc(w.what)}</div><div class="why">${esc(w.meta)}</div></div></div>`,)}</div></div>`}/** One card per kind, so "what kind of work is this" stops needing the meta line. */function workByKind(d: MyDay): string {const kinds = (['order', 'task', 'todo', 'tracker'] as const).map((k) => ({ k, rows: d.items.filter((w) => w.kind === k) })).filter((g) => g.rows.length)if (!kinds.length) return `<p class="empty" style="padding:10px 0">${emptyBoard(d)}</p>`return map(kinds,(g) => `<div class="kind"><div class="kind-head"><span class="swatch" style="background:${KIND[g.k].hue}"></span>${esc(KIND[g.k].label)}<span class="c">${g.rows.length}</span></div>${map(g.rows,(w) => `<div class="work-row"${w.noteKey ? ` data-note-key="${esc(w.noteKey)}"` : ''}><div><div class="work-what">${esc(w.what)}</div><div class="work-meta">${esc(w.meta)}</div></div><div class="work-age">${esc(w.age)}${w.late ? '<span class="late">late</span>' : ''}</div></div>`,)}</div>`,)}/*** Meeting action items as topic buttons, each opening its own panel, rather* than one long list at the bottom of My Day. Buckets come from* `config/fathom-topics.ts`, a hand-edited table.** The buttons are built from `d.commitments` and nothing else, and that is a* security property rather than a convenience. `commitments` has already been* through `meetingsFor()`, which is the boundary for meeting content on this* board. Deriving a stable, board-wide set of buttons from `snap.meetings` would* disclose the existence of meetings this viewer was not in. So two people see* different buttons, and that is correct.** A separate `data-topic` namespace, not the `data-drill` one it is modelled on:* that handler closes every `[data-drill-panel]` on the page and `drillPanel()`* hardcodes `id="drill"`, so reusing it would make opening a topic silently* close a headline tile's drill.*/function topicButtons(buckets: TopicBucket<MyDay['commitments'][number]>[]): string {const total = buckets.reduce((n, b) => n + b.items.length, 0)return `<div class="topic-row">${map(buckets,(b) => `<button type="button" class="topic-btn" data-topic="${esc(b.slug)}"aria-expanded="false" aria-controls="topic-${esc(b.slug)}">${esc(b.label)}<span class="n">${b.items.length}</span></button>`,)}</div><p class="note" style="margin:8px 0 0">${total} ${plural(total, 'item')} in ${buckets.length} ${plural(buckets.length, 'topic')}:every one is in exactly one, so the numbers above add up to ${total}. Topics are keywordrules in <code>config/fathom-topics.ts</code>; anything they miss lands in<b>${esc(UNCATEGORIZED)}</b> rather than being guessed at.</p>${map(buckets,(b) => `<div class="topic-panel" id="topic-${esc(b.slug)}" data-topic-panel="${esc(b.slug)}" hidden><div class="topic-head"><span class="n">${b.items.length}</span><span class="t">${esc(b.label)}</span><span class="m">${b.label === UNCATEGORIZED? 'No keyword rule matched these. That is a gap in the map, not a category.': 'From meetings you were in'}</span><button type="button" data-topic-close>Close</button></div><div class="topic-body">${map(b.items,(c) => `<div class="row" style="grid-template-columns:1fr auto auto"><span>${esc(c.text)}</span><span class="note" style="white-space:nowrap">${c.unowned? `<b style="color:${C.magenta}">nobody named</b>`: c.owner? esc(c.owner): c.externalOwner? `${esc(c.externalOwner)} <span class="quiet">(external)</span>`: `<span class="quiet">${esc(c.unknownOwner ?? 'unrecognized')}</span>`}</span><span class="note" style="white-space:nowrap"><a href="${esc(c.url)}">${esc(c.meeting.slice(0, 28))}</a> · ${esc(localStamp(c.date))} CT</span></div>`,)}</div></div>`,)}`}function myDayCard(d: MyDay,dateShort: string,written?: string,cutOff: string | null = null,viewer: Viewer = COMPANY_VIEWER,): string {const p = d.person// Not `hidden`. The page carries exactly the one card the viewer may read,// so hiding it would hide the only thing there is.return `<div class="card" data-person-panel="${esc(p.name)}"><div class="rule"></div><div class="myday-head"><div class="who"><div class="eyebrow" style="color:var(--on-slab-dim)">My Day · ${esc(dateShort)}</div><h1>${esc(p.name)}</h1><div class="cover">${esc(p.role)} · covered by ${esc(p.coverage.join(', ') || 'no source')}</div></div><div class="myday-nums"><div class="b"><div class="v">${p.load === null ? '–' : p.load}</div><div class="k">on my board</div></div><div class="b"><div class="v" style="color:${d.lateCount ? C.magenta : 'inherit'}">${d.lateCount}</div><div class="k">past a review gate</div></div></div></div>${written? `<div style="padding:16px 28px 0"><details class="brief-inline" open><summary><span class="caveat-chip" style="letter-spacing:.14em">Briefing</span>Written for you</summary>${cutOff ? `<div style="padding:0 18px">${caveat(cutOff)}</div>` : ''}<div class="body prose">${map(written.split(/\n\s*\n/).filter((x) => x.trim()),(x) => `<p>${esc(x.trim()).replace(/\n/g, '<br>')}</p>`,)}</div></details></div>`: ''}${firstThree(d)}<div style="padding:16px 28px 0"><div class="chips"><span class="chip solid">${d.ourMove} ${plural(d.ourMove, 'order')}</span><span class="chip">${d.tasks} ${plural(d.tasks, 'task')}</span><span class="chip">${d.todos} ${plural(d.todos, 'to-do')}</span><span class="chip">${d.tracker} ${plural(d.tracker, 'tracker item')}</span>${d.waiting ? `<span class="chip hatch">${d.waiting} waiting on client, not mine today</span>` : ''}</div>${workByKind(d)}${d.waiting? `<p class="note" style="margin-top:10px">${d.waiting} order ${plural(d.waiting, 'line')} on ${esc(p.name.split(' ')[0] ?? p.name)}’splate ${plural(d.waiting, 'sits', 'sit')} with the client and ${plural(d.waiting, 'is', 'are')} not listed here; they are counted in the chips above and shown on The queue.</p>`: ''}${d.todosWithheld? `<p class="note" style="margin-top:10px">${d.todosWithheld} Ninety ${plural(d.todosWithheld, 'to-do')} ${plural(d.todosWithheld, 'is', 'are')} counted in the number above but not listed here: only the person themselves sees their own to-do list on this board.</p>`: ''}</div><div style="padding:20px 28px 0">${// The private list. A placeholder at sync time; the Worker swaps it for// the viewer's own lines at serve time, so no private text is ever// written into the pre-rendered page in KV.//// The ownership test is the load-bearing half, and it looks redundant.// myDaySurface filters to the viewer's own card, so today the two// conditions are the same and it is tempting to write only the second.// There is one marker pair per page and the Worker stamps the viewer's// list into it: the day anything renders a second person's card (a// manager view, a print-all, a debug flag), a placeholder sitting in// that card would put the viewer's private list under somebody else's// name. One comparison makes that impossible rather than merely absent.d.person.name === viewer.ownBriefing && hasOwnList(viewer) ? ownListPlaceholder() : ''}</div><div style="padding:20px 28px 0"><div style="font-size:13px;font-weight:600;margin-bottom:8px">Said in a meeting</div>${!d.commitmentsTracked? // Not tracked, not empty. No Fathom key or a failed fetch reads as// "we did not look"; an empty list here would read as "you owe// nothing", which is the prime invariant's whole point. It must not// render as a row of zero topic buttons either: no buttons and no// key look identical, and they are opposite facts.`<div class="card" style="padding:4px 16px"><p class="empty" style="padding:10px 0">Meeting commitments are <b>not tracked</b>: no Fathom key is configured, or the fetch failed. This is not a clear desk.</p></div>`: d.commitments.length? topicButtons(bucketByTopic(d.commitments)): `<div class="card" style="padding:4px 16px"><p class="empty" style="padding:10px 0">Nothing came out of your meetings in the window.</p></div>`}${// A dark key is said here, not only in the source band. The list above is// short by whatever that key would have returned, and short is the one// thing this card cannot tell apart from a light fortnight on its own.d.commitmentsPartial? `<p class="note" style="margin-top:8px;color:${C.magenta}">${esc(d.commitmentsPartial)}</p>`: ''}<p class="note" style="margin-top:8px">Every action item from meetings <b>you were in</b>, last ${14} days, not only the onesassigned to you, because you were in the room for all of them. Meetings you were not in donot appear here at all. An item with <b>nobody named</b> belongs to someone in that room;no other system will ever surface it. There is no completion state on these, so they dropoff after the window whether or not they happened.</p></div><div style="margin-top:22px;background:var(--slab);color:var(--on-slab);padding:16px 28px;font-size:12.5px;line-height:1.6"><b>Nothing here closes outside a Monday review.</b>${d.lateCount? `${d.lateCount} ${plural(d.lateCount, 'item')} ${plural(d.lateCount, 'is', 'are')} past a review gate, which is what “late” means at Meridian, not calendar days.`: 'Nothing is past a review gate, which is what “late” means at Meridian, not calendar days.'}An open to-do may be finished work waiting for sign-off, so the age column counts gates missed, and says so on the row.</div></div>`}/*** One person's day: theirs, and nobody else's.** The page is rendered per viewer and contains one card. Every briefing* addresses its reader directly as "you", so a page holding every card with* the others hidden would let anyone read them all with view-source: hiding is* presentation, not access control. There is no person picker, because there* is nothing to pick between.*/export function myDaySurface(vm: ViewModel,briefings: Briefings | null = null,viewer: Viewer = COMPANY_VIEWER,): string {const mine = viewer.ownBriefing? vm.myDays.filter((d) => d.person.name === viewer.ownBriefing): []if (!mine.length) {// Two different absences, said differently. An unrecognised address is a// roster gap somebody should close; a recognised person with no card means// no source covers them, which is a coverage fact rather than a permissions// one. Neither renders as an empty board.const why = viewer.person? `No per-person view is built for ${esc(viewer.person.name)}: no source on this ` +`board covers their work individually.`: `This view is not signed in as anyone on the roster, so there is no personal ` +`briefing to show. Company numbers, the queue and data health are all here. ` +`If you should have a personal view, ask Sam to add your address to the roster.` +` There is no private list here either: it is per person, and this address is a ` +`shared mailbox rather than a person, and a private list on a shared mailbox is a list ` +`everyone who signs into it can read, which is the opposite of what it is for.`return join([`<section class="section"><div class="section-head"><h2>My day</h2><span class="hint">your work only: orders, tasks, to-dos and tracker items in one list</span></div><div class="card" style="padding:22px 28px"><p class="empty">${why}</p>${// A roster person with no card still gets their list. Every roster// person has a card today, because personViews builds from the// roster; this branch is reachable only if that ever stops being// true, and the people it would drop are the ones no source covers,// the very people the say-what-you-cannot-see rule was written// for, and exactly who most needs somewhere to write things down.hasOwnList(viewer) ? ownListPlaceholder() : ''}</div></section>`,])}return join([`<section class="section"><div class="section-head"><h2>My day</h2><span class="hint">your work only: orders, tasks, to-dos and tracker items in one list</span></div>${map(mine, (d) =>myDayCard(d,vm.briefing.dateShort,briefings?.briefings?.[d.person.name],briefings ? truncatedNote(briefings, d.person.name) : null,viewer,),)}</section>`,])}// --- onboarding -------------------------------------------------------------------/*** The onboarding tab: a tab beside Open work holding a new person's training* material, switched off as they stop being new.** The other surfaces render a snapshot of live sources, and their hardest job* is not turning an absence into a zero. This one renders a hand-written* document, so its hardest job is the opposite: not implying anything the* document does not say. The material is dated and attributed at the foot.* Where it is silent, this is silent.** Visibility is decided in config/onboarding.ts, not here. `onboardingFor`* takes the viewer and returns what they may read; a viewer with nothing gets* no tab at all (render.ts omits it from the markup, the same way the inbox tab* is omitted rather than hidden). This function is never called for them, and* the empty state below exists for the one case that can happen: a plan that is* on, for a person, with nothing written in it yet.*/export function onboardingSurface(viewer: Viewer = COMPANY_VIEWER): string {const plans = onboardingFor(viewer)if (!plans.length) {// Reachable only by a direct call; the tab is not rendered without plans.return `<section class="section"><div class="section-head"><h2>Onboarding</h2></div><div class="card" style="padding:22px 28px"><p class="empty">Nobody is being onboarded right now.</p><p class="note" style="margin-top:8px">This tab appears while someone's plan is switchedon in <code>config/onboarding.ts</code>. Switching it off keeps the material and hidesthe tab.</p></div></section>`}return `<section class="section"><div class="section-head"><h2>Onboarding</h2><span class="hint">training material, kept as it comes in</span>${// A short recap of what has been covered so far. Linked from here// rather than being a tab, because it is a view of this material and// not another place to look. Everyone who// can see this tab can open it: it carries training topics only, so// there is nothing on it they cannot already read below.`<a class="hint" href="/onboarding/export" style="margin-left:auto">Training recap →</a>`}</div>${// The end-of-day note goes first, above the material. It is the one thing// on this tab somebody has to do today; the plan below it is reference the// trainee has already read. Rendered as a placeholder the Worker fills in at serve// time, because a card baked into a nightly render would show a form for a// day that had not started yet (see checkin-view.ts).//// It is omitted entirely for anyone with neither grant, which is not the// same list as the people who can see this tab (readers of Leadership// to-dos see the plan). Nothing is emitted for them: not a hidden div,// nothing.hasCheckin(viewer) ? checkinPlaceholder() : ''}${map(plans, (p) => onboardingCard(p, viewer))}</section>`}/*** "Sep 14" from a bare calendar date. Defined in @meridian/ninety-client alongside* every other date helper, and re-exported here because two surfaces format* bare calendar dates now, and because this is the import path the tests and* the rest of this app already use.*/export { calendarDate }/** Day 1 on the start date, and never day 0: a person's first day is their first. */function dayNumber(startedOn: string, now: Date): number | null {const start = Date.parse(`${startedOn}T00:00:00Z`)if (Number.isNaN(start)) return nullconst today = Date.parse(`${localDay(now.toISOString())}T00:00:00Z`)return Math.max(1, Math.round((today - start) / 86_400_000) + 1)}function onboardingCard(plan: OnboardingPlan, viewer: Viewer, now: Date = new Date()): string {const day = dayNumber(plan.startedOn, now)const isMine = plan.person === viewer.person?.namereturn `<div class="card" style="margin-bottom:18px"><div class="rule"></div><div style="padding:22px 28px 16px;border-bottom:1px solid var(--border-default)"><div class="eyebrow">${esc(plan.role)}${plan.trainedBy ? ` · trained by ${esc(plan.trainedBy)}` : ''}</div><h1 style="font-family:var(--font-display);font-size:26px;color:var(--text-body)">${esc(isMine ? 'Your first weeks' : `${plan.person}: onboarding`)}</h1><p class="note" style="margin-top:6px">Started ${esc(calendarDate(plan.startedOn))}${day === null ? '' : ` · day ${day}`}.${// Say what is shown, in its smallest form: a reader looking at somebody else's// training should know that is what they are looking at.isMine ? '' : `Shown to you because you read the Leadership Team's to-dos.`}</p><p style="margin:10px 0 0;font-size:13px;color:var(--meridian-gray-700);max-width:78ch">${esc(plan.summary)}</p></div><div style="padding:6px 28px 0">${map(plan.days, (d) => onboardingDay(d))}${plan.days.length? '': `<p class="empty" style="padding:16px 0">Nothing has been added to this plan yet.That is an empty file, not an empty first week. The material goes into<code>config/onboarding.ts</code> as it comes in.</p>`}${onboardingResources(plan.resources)}</div><p class="note wide" style="padding:16px 28px 24px;border-top:1px solid var(--hairline)"><b>Where this came from:</b> ${esc(plan.sources.join(' · '))}.The SOPs are the source of truth: where this and an SOP disagree, follow the SOP andtell Sam. This page is a config file(<code>config/onboarding.ts</code>), so it changes when somebody edits it and not before.</p></div>`}/*** One day, or one standing group: a collapsible holding collapsible topics.** `<details>` and not a script, so both levels fold without JS. The board ships as one self-contained `dashboard.html` that is also served by* the Worker, and a disclosure widget the browser already implements needs no* JS, survives a file:// open, keeps its state through a print, and is* keyboard-navigable without anybody writing that. The `open` attribute is the* data's, not the renderer's; see the field comment in config/onboarding.ts.** What the summary has to carry: a folded thing is a promise about what is* inside it, so the day says how many topics it holds and each topic says how* many points, counted off the very items rendered below, never off anything* else. A heading you have not opened should still be able to account for* what it is hiding.*/function onboardingDay(day: {day: number | nulldate: string | nulltitle: stringintro?: stringopen?: booleanrecordings?: { label: string; href: string; note?: string }[]sections: {title: stringintro?: stringbullets?: string[]rows?: { label: string; detail: string; tag?: string }[]}[]}): string {// A day carries its number and date; standing material carries neither, and// says so rather than borrowing a day's vocabulary for something that is true// every day. "Ongoing" and not "Every day": the first standing group is// titled "Every day: the operating rhythm", and an eyebrow that repeats the// heading it labels is noise in the one place a scanner is relying on it.const eyebrow =day.day === null? 'Ongoing': `Day ${String(day.day).padStart(2, '0')}${day.date ? ` · ${esc(calendarDate(day.date))}` : ''}`const topics = day.sections.lengthreturn `<details class="ob-day"${day.open ? ' open' : ''}><summary class="ob-day-sum"><span class="ob-day-eyebrow">${eyebrow}</span><span class="ob-day-title">${esc(day.title)}</span><span class="ob-count">${topics} ${topics === 1 ? 'topic' : 'topics'}</span></summary><div class="ob-day-body">${day.intro ? `<p class="ob-intro">${esc(day.intro)}</p>` : ''}${day.recordings?.length? `<p class="ob-rec">${map(day.recordings,(r) =>`<a href="${esc(r.href)}" target="_blank" rel="noreferrer noopener">${esc(r.label)}</a>`,)}</p>`: ''}${map(day.sections, (sec) => onboardingSection(sec))}${topics? '': `<p class="empty">Nothing written under this one yet.</p>`}</div></details>`}function onboardingSection(sec: {title: stringintro?: stringbullets?: string[]rows?: { label: string; detail: string; tag?: string }[]}): string {// Counted off exactly what is rendered underneath, so the two can always// account for each other.const points = (sec.bullets?.length ?? 0) + (sec.rows?.length ?? 0)return `<details class="ob-topic"><summary class="ob-topic-sum"><span class="ob-title">${esc(sec.title)}</span><span class="ob-count">${points}</span></summary><div class="ob-topic-body">${sec.intro ? `<p class="ob-intro">${esc(sec.intro)}</p>` : ''}${sec.bullets?.length? `<ul class="ob-list">${map(sec.bullets, (b) => `<li>${esc(b)}</li>`)}</ul>`: ''}${sec.rows?.length? map(sec.rows,(r) => `<div class="ob-row"><div class="ob-label">${esc(r.label)}${r.tag ? `<span class="ob-tag">${esc(r.tag)}</span>` : ''}</div><div class="ob-detail">${esc(r.detail)}</div></div>`,): ''}${points ? '' : `<p class="empty">Nothing under this heading yet.</p>`}</div></details>`}/*** The master resources list, at the foot of the plan.** Not collapsed. Everything above it folds because it is material you read once* and then skip; this is the part you come back to mid-task, and a link you* have to expand twice to reach is a link you stop using. It renders open, and* it renders nothing at all when there are no links; an empty "Resources"* heading promises a list that does not exist.*/function onboardingResources(groups: { title: string; links: { label: string; href: string; note?: string }[] }[]): string {const withLinks = groups.filter((g) => g.links.length)if (!withLinks.length) return ''return `<div class="ob-resources"><h3 class="ob-res-head">Resources</h3><p class="ob-intro">Everything linked anywhere on this plan, in one place.</p>${map(withLinks,(g) => `<div class="ob-res-group"><h4 class="ob-res-title">${esc(g.title)}</h4>${map(g.links,(l) => `<div class="ob-row"><div class="ob-label"><a href="${esc(l.href)}" target="_blank" rel="noreferrer noopener">${esc(l.label)}</a></div><div class="ob-detail">${esc(l.note ?? '')}</div></div>`,)}</div>`,)}</div>`}// --- 1e: what moved elsewhere ------------------------------------------------------export function elsewhere(vm: ViewModel): string {return `<div class="card"><div class="rule"></div><div style="padding:22px 28px 16px;border-bottom:1px solid var(--border-default)"><div class="eyebrow">Elsewhere</div><h1 style="font-family:var(--font-display);font-size:26px;color:var(--text-body)">What moved outside your lane</h1><p style="margin:6px 0 0;font-size:13px;color:var(--meridian-gray-700);max-width:78ch">Last 24 hours. Neither Plinth nor Bitrix keeps an event log, so this is built from thetimestamps that do exist (<code>updatedAt</code> on a lead, the order date on a line)plus standing facts worth knowing whenever they changed. It is an honest“what looks new”, not a changelog, and anything the source does not timestamp islisted with the time left blank rather than with one invented for it.</p></div><div style="padding:6px 28px 0">${vm.feed.length? map(vm.feed,(f) => `<div class="feed-row"><div><span class="badge ${f.tone}">${esc(f.dept)}</span><div class="feed-time">${esc(f.time)}</div></div><div><div class="feed-text">${esc(f.text)}</div><div class="feed-why">${esc(f.why)}</div></div></div>`,): '<p class="empty" style="padding:16px 0">Nothing moved in the last 24 hours that another department would need to know.</p>'}</div><p class="note" style="padding:16px 28px 24px">Nothing here needs an action from you. It exists so that Growth does not hear about a deliveredorder two weeks after Client Success closed it.</p></div>`}// --- 1f: data health ----------------------------------------------------------------export function dataHealth(vm: ViewModel): string {const gaps = vm.coverage.filter((r) => r.cells.some((c) => c.text === 'not tracked'))return `<div class="card"><div class="rule"></div><div style="padding:22px 30px 18px;border-bottom:1px solid var(--border-default)"><div class="eyebrow">Data Health</div><h1 style="font-family:var(--font-display);font-size:26px;color:var(--text-body)">What this board can see</h1><p style="margin:6px 0 0;font-size:13px;color:var(--meridian-gray-700);max-width:74ch">Every number on the dashboard traces back to one of these. A source that fails shows asfailed. Zero and broken must never look the same.</p></div><div style="padding:8px 30px 0">${map(vm.sources,(s) => `<div class="source-row"><div class="health ${s.tone === 'failed' ? 'failed' : s.tone === 'degraded' || s.tone === 'absent' ? 'stale' : /read/i.test(s.status) ? 'read' : 'live'}"title="${esc(s.status)}"></div><div><div class="source-name">${esc(s.name)}</div><div class="source-covers">${esc(s.covers)}</div></div><div class="source-note">${esc(s.note)}</div><div class="source-status">${esc(s.status)}</div></div>`,)}</div><div style="padding:22px 30px 0"><h2 style="font-size:14px;font-weight:600">People coverage</h2><p class="note" style="margin:4px 0 12px">"Covered" means the source can see this person, not that they have work. Somebody coveredwith nothing open is idle; somebody not tracked is invisible, and those must never look thesame. Ninety is the only source here that sees everyone. Plinth sees only people holdingorder lines, and Bitrix tasks only people with tasks.</p><div class="matrix"><div class="matrix-head"><div>Person</div>${map([...COVERAGE_COLUMNS], (h) => `<div>${esc(h)}</div>`)}</div>${map(vm.coverage,(r) => `<div class="matrix-row"><div>${esc(r.name)}</div>${map(r.cells, (c) =>c.text === 'covered'? `<div class="cell-covered" style="font-size:12px"><i aria-hidden="true"></i>covered</div>`: c.text === '–'? `<div style="font-size:12px;color:var(--meridian-gray-400)">not applicable</div>`: `<div><span class="cell-untracked">${esc(c.text)}</span></div>`,)}</div>`,)}</div>${gaps.length? `<div class="caveat" style="margin-top:16px"><b>${esc(gaps.map((g) => g.name.split(' ')[0]).join(' and '))} hold no Plinth assignment.</b><span>Their order work is invisible here. Their small numbers are a coverage gap, notspare capacity. Do not route work off that row.</span></div>`: ''}</div><div style="padding:20px 30px 26px"><h2 style="font-size:14px;font-weight:600;margin-bottom:10px">Known blind spots</h2><div class="blindspots"><div class="blindspot"><div class="strip"></div><h3>Mockups in flight</h3><p>No system records it. The design queue (${vm.headline.designQueue}) is the closest proxyand it is not the same thing. Ask the designers.</p></div><div class="blindspot"><div class="strip"></div><h3>Ninety rocks and to-dos</h3><p>One token sees one person. A company view needs one token per person. That is theAPI’s shape, not a workaround.</p></div><div class="blindspot"><div class="strip"></div><h3>Scorecard / KPI trends</h3><p>Write-only in the Ninety API. There is no way to read values back.</p></div><div class="blindspot"><div class="strip"></div><h3>A future CRM change</h3><p>Nothing here assumes Bitrix is permanent. The stage taxonomy is one config file, so a CRMswap is a rewrite of that file, not of the board.</p></div></div></div></div>`}// --- 1g: the queue ------------------------------------------------------------/*** Order numbers, grouped under whoever is holding them.** This is the surface a designer opens. Everything else on this dashboard* describes the day; this one lists the work. The rules it is built on:** Real tables, not divs. Screen readers announce row and column position from* a <table> with proper headers and get nothing from a grid of divs. Someone* navigating this by keyboard needs to hear "row 4, order MD2639511" rather* than a stream of unlabelled text.** Age is not conveyed by colour. An aging row gets a text marker as well as* weight, because "the red ones are old" is not information for a colourblind* reader or a printed page.** Absence is a value. "unassigned" and "product not in export" are printed* words. A blank cell in a designer column reads as "no designer needed",* which is the exact opposite of what it means.** No decorative numbers. Every figure on this surface is something a person* can act on or verify. A count nobody can do anything with is noise, and* noise is what makes people stop reading a dashboard.*/export function queueSurface(vm: ViewModel): string {const q = vm.queueif (!q.available) {return `<section class="card"><div class="rule"></div><div style="padding:22px 28px 20px"><div class="eyebrow">The Queue</div><p class="empty">${esc(q.reason)}</p></div></section>`}// The age system: the number, then a 3px bar over a fixed 60-day field with a// hairline at 30, so 54d and 5d differ before either number is read. The flag// stays a word, because the page prints and roughly one in twelve men reading// it will not see the magenta.const ageCell = (r: QueueRowView): string => {if (r.ageDays === null) return `<td class="age-cell">${esc(r.age)}</td>`const pct = Math.min(100, (r.ageDays / 60) * 100)return `<td class="age-cell" data-label="Age"><span class="d">${r.ageDays}</span><span class="u">d</span><div class="age-track" aria-hidden="true"><i style="width:${pct.toFixed(1)}%"></i></div>${r.aging ? '<span class="aging-flag">aging</span>' : ''}</td>`}const row = (r: QueueRowView): string => `<tr${r.aging ? ' class="aging"' : ''} data-note-key="order:${esc(r.orderNumber)}" style="box-shadow:inset 3px 0 0 ${r.statusColor}"><th scope="row" class="ordnum" style="padding-left:12px">${esc(r.orderNumber)}</th><td data-label="Product">${esc(r.product)}</td><td data-label="Status"><span class="status-dot" style="background:${r.statusColor}" aria-hidden="true"></span>${esc(r.status)}</td><td data-label="Client">${esc(r.customer)}</td>${ageCell(r)}</tr>`const group = (g: QueueGroupView): string => `<section class="queue-group${g.unassigned ? ' queue-unassigned' : ''}"aria-labelledby="q-${esc(g.designer.replace(/\W+/g, '-').toLowerCase())}"><h3 id="q-${esc(g.designer.replace(/\W+/g, '-').toLowerCase())}" class="queue-head">${g.unassigned ? '' : `<span class="avatar" aria-hidden="true">${esc(g.designer.split(/\s+/).map((w) => w[0] ?? '').join('').slice(0, 2).toUpperCase())}</span>`}${esc(g.designer)}<span class="queue-sub">${esc(g.summary)}</span>${g.rows.filter((r) => r.aging).length ? `<span class="queue-aging-count">${g.rows.filter((r) => r.aging).length} aging</span>` : ''}</h3><table class="queue-table"><caption class="sr-only">Order lines held by ${esc(g.designer)}, oldest first</caption><thead><tr><th scope="col">Order</th><th scope="col">Product</th><th scope="col">Status</th><th scope="col">Client</th><th scope="col">Age</th></tr></thead><tbody>${map(g.rows, row)}</tbody></table></section>`return join([// Same header anatomy as Elsewhere and Data Health (rule, eyebrow, display// title, muted lede), so every tab looks like the same product.`<section class="card"><div class="rule"></div><div style="padding:22px 28px 20px"><div class="eyebrow">The Queue</div><h1 style="font-family:var(--font-display);font-size:26px;color:var(--text-body)">Order lines we owe the next move on</h1><p style="margin:6px 0 0;font-size:13px;color:var(--meridian-gray-700);max-width:78ch">Oldest first, grouped by the designer holding them. Order numbers are the ones youcan paste into Plinth. Lines waiting on a client or sitting with a vendor aredeliberately not here.</p>${// When lines are unassigned, the magenta notice below carries that fact// with the fix attached; repeating the same sentence here word for// word would read like a rendering bug.q.unassignedCount ? '' : `<p class="note" style="margin-top:8px">${esc(q.reason)}</p>`}${q.everythingIsOld ? `<p class="note" style="margin-top:8px;color:${C.magenta}">${esc(q.everythingIsOld)}</p>` : ''}</div></section>`,// Unassigned lists last so it never reads as the team's biggest queue, and// it gets the notice above. It means Plinth itself holds no assignee.q.unassignedCount? `<div class="unassigned-notice"><span class="n">${q.unassignedCount}</span><span><b>of ${q.total} order lines carry no designer.</b> Plinth returned <code>--</code> for theselines. They are genuinely unassigned, per line, straight from the API. The fix is to assign them in Plinth;there is nothing to re-export. This group lists last, so it never reads as theteam’s backlog.</span></div>`: '',q.groups.length? map([...q.groups.filter((g) => !g.unassigned), ...q.groups.filter((g) => g.unassigned)], group): '<section class="card"><p class="empty">Nothing is waiting on this team right now.</p></section>',])}// --- the written briefing ------------------------------------------------------/*** Turn the model's plain prose into paragraphs. It is instructed to write plain* sentences with no markup, so this only splits on blank lines and escapes.*/const asProse = (text: string): string =>map(text.split(/\n\s*\n/).filter((p) => p.trim()),(p) => `<p>${esc(p.trim()).replace(/\n/g, '<br>')}</p>`,)/*** A stale pairing is worse than a missing one, but say which kind.** The briefing is written from one sync. If the board has re-synced since, the* prose describes numbers that are no longer the ones beside it, and a reader* has no way to tell. So a mismatch is stated rather than styled around.** Two messages rather than one, because the board re-syncs every 15 minutes* while the prose is authored once each morning, so a gap is the normal state* from about 06:15 onward. A single "re-run the briefing" warning would fire* all day and ask for something nobody wants (a model call every quarter* hour), and a warning that is always on stops being read.** So: prose from earlier today is expected and reads as a note about scope.* Prose from an earlier day means the morning brief did not run, which is a real* problem with a real fix. Only the second one asks for anything.*/function briefingAge(b: Briefings, snap: Snapshot): string | null {if (!b.payloadGeneratedAt) return nullif (b.payloadGeneratedAt === snap.syncedAt) return nullconst writtenAt = clock(b.payloadGeneratedAt)if (localDay(b.payloadGeneratedAt) === localDay(snap.syncedAt)) {return (`Written at ${writtenAt} from that sync's numbers. The counts on the rest of this page ` +`refresh every 15 minutes, so they have moved since. This reads the morning, not the ` +`minute.`)}return (`Written ${shortDate(b.payloadGeneratedAt)} at ${writtenAt}, an EARLIER DAY, not this ` +`morning. The numbers beside it are today's. Re-run \`pnpm morning\` before trusting it.`)}const clock = (iso: string): string =>new Intl.DateTimeFormat('en-US', { timeZone: MERIDIAN_TZ, hour: 'numeric', minute: '2-digit' }).format(new Date(iso),)/*** The caveat for a briefing the model did not finish.** Kept separate from briefingAge() because the two say different things and can* both be true at once: one is "written from older numbers", the other is* "written, but cut off mid-thought". A reader needs to know which.*/function truncatedNote(b: Briefings, key: string): string | null {if (!b.truncated?.includes(key)) return nullreturn ('Cut off at the model output limit. This briefing stops mid-thought and ' +'the rest of it was never written. Re-run the briefing to get all of it.')}/** The `caveat down` treatment used for both briefing caveats. */function caveat(text: string): string {return `<p class="caveat down" style="margin:10px 0">${esc(text)}</p>`}/*** The company briefing, inline at the top of Open work, read where the work* is, per the approved IA. A real <details open>, so it collapses with* scripting off. The Briefing tab keeps the full version as archive.** The staleness caveat names the specific conflict rather than a vague "may be* stale": prose that says one number beside tiles that say another is the* fastest way to lose a reader's trust in both.*/export function inlineBriefing(b: Briefings | null,snap: Snapshot,viewer: Viewer = COMPANY_VIEWER,): string {const text = b?.briefings?.[companyBriefingKey(viewer)]if (!b || !text) return ''const stale = briefingAge(b, snap)const firstPara = text.split(/\n\s*\n/)[0] ?? textreturn `<details class="brief-inline" open><summary><span class="caveat-chip" style="letter-spacing:.14em">Briefing</span>This morning, in prose <span class="hint">· ${esc(localClock(b.generatedAt))} CT · full version on the Briefing tab</span></summary>${stale ? `<div style="padding:0 18px">${caveat(stale)}</div>` : ''}<div class="body">${esc(firstPara.trim()).replace(/\n/g, '<br>')}</div></details>`}/*** The model-written briefing, shown above the generated one.** Both are on the page because they are different artifacts and only one of* them can be wrong in an interesting way. The block below this is assembled* from the numbers by `briefingProse()`. It cannot hallucinate but it also* cannot notice anything. This one is written by a model and is the one to* read critically. Keeping them adjacent means a drift between them is visible.*/export function writtenBriefing(b: Briefings | null,snap: Snapshot,viewer: Viewer = COMPANY_VIEWER,): string {// Two company briefs exist because prose cannot be redacted after a model// writes it. `company` names Leadership Team to-dos by title; the all-hands one// is generated from a payload with them removed. The viewer decides which,// and there is no fallback from one to the other: an absent all-hands brief// renders as absent rather than quietly serving the full one.const companyKey = companyBriefingKey(viewer)const text = b?.briefings?.[companyKey]if (!b || !text) {return `<section class="card"><div class="rule"></div><div style="padding:22px 28px 20px"><div class="eyebrow">Written Briefing</div><p class="empty">Not generated for this sync. Run<code>python3 analysis/brief.py</code> (or <code>pnpm morning</code>, whichsyncs, writes every briefing, and re-renders this page).</p></div></section>`}const stale = briefingAge(b, snap)const cutCompany = truncatedNote(b, companyKey)const cutExec = truncatedNote(b, 'exec')// An eyebrow, not a second display h2: the tab header above this card already// says "Briefing" in 27px caps.return `<section class="card"><div class="rule"></div><div style="padding:22px 28px 24px"><div style="display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap"><div class="eyebrow" style="margin-bottom:0">Written Briefing</div><span class="note">${esc(b.model)} · ${esc(localClock(b.generatedAt))} CT</span></div>${stale ? caveat(stale) : ''}${cutCompany ? caveat(cutCompany) : ''}<div class="prose" style="margin-top:12px">${asProse(text)}</div>${// Enforced, not styled: the exec brief is omitted from the markup for// everyone without seesExec: not collapsed, not hidden. A <details> element is open to// anyone who clicks it, and its contents are in the page either way.viewer.seesExec && b.briefings.exec? `<details style="margin-top:16px"><summary style="cursor:pointer;font-weight:600;font-size:13px">Leadership view</summary>${cutExec ? caveat(cutExec) : ''}<div class="prose" style="margin-top:10px">${asProse(b.briefings.exec)}</div></details>`: ''}</div></section>`}