Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Home reads like a day's start (#54): what needs you first with the one to start on at the top, then one card for what happened, and what is running, what it cost and what settled by itself in a side column that stays in view.

The span was summarized five times (the sentence under Ask, What changed, While you were away, People, Landed's line) and production builds were listed three times (their own Landed row, the merge's Live label, Deploys). Now: - Where you're needed opens with Start here, set apart with why it was picked and its button; the separate Start here card is gone. - Since you were last here is one card: People (two columns of lines, the faces), What changed as a strip with Summarize, and the Landed feed, whose line counts what is live, deploying, waiting and failed (what While you were away's chips said). - A production build is a Landed row only where no merge or push on the list already says it (its live label links to the build, or its commit is the build's): a redeploy, a rollback, or a failed build, shown red (landedDeploys). The Deploys card is gone; previews stay on the project's Deployments. - Running now (wrapping titles, state and time under them), Spent and Settled for you sit in a sticky side column on wide screens; on a phone it is Where you're needed, Running now, the span, then Spent and Settled. - Done links to /notifications/done, the path #52 moves it to.

Checked:
rendered the route with fixture data in a Vite harness at 1440 and 390 (dark), no console errors, no sideways scroll at 390; tsc -b clean; the web tests (963) pass, including the new landedDeploys case; the docs build passes.
G1t-Session:
ses_01m4ypk0jhex5t0j2c38w7aq7p
syntaqxcommitted Parenta35ae67Browse files
7 files+300−3530/7 viewed
+57−62
55
66 **Home** is the first page of your workspace, at `/<workspace>/-/home`.
77 It opens with **What are we building today?** and a box that
8−[asks g1t](#ask-from-home), then answers three questions, in this order:
8+[asks g1t](#ask-from-home). Under it, the main column is read top to
9+bottom:
910
1011 1. **Where you're needed**: everything waiting on you, however long ago
11− it started, with the action beside each item.
12−2. **What happened while you were away**: what changed, in plain
13− English when you ask for it; what settled by itself and what went
14− live; what people did; what landed; deploys and spend, since you were
15− last here.
16−3. **What is running now**: workflow runs and builds still going.
12+ it started, with the action beside each item. Its first row is
13+ [Start here](#start-here), the one item to do first, and why.
14+2. **Since you were last here**: one card for the span. It shows what people
15+ did, what changed in plain English when you ask for it, and what landed,
16+ each change with whether it is live yet.
17+
18+The side column stays in view as you scroll:
19+
20+- **[Running now](#running-now)**: workflow runs and builds still going.
21+- **[Spent](#spent)**: what the span cost, and the month so far.
22+- **[Settled for you](#settled-for-you)**: what finished by itself.
1723
18−**Start here** picks the one item to do first.
24+On a phone it is one column: Where you're needed, Running now, the span,
25+then Spent and Settled for you.
1926
2027 For a workspace's owners, Home also opens
2128 [Getting started](/guides/workspaces/#getting-started), the setup guide,
8895
8996 ## Start here
9097
91−**Start here** is the first row of Where you're needed: the most pressing
92−tier, and within it the row that has waited longest. g1t picks it with
93−that rule, so the same rows always give the same pick.
94−The card shows who is on it (**Owner**), the step it waits for (**Next**,
95−such as **Merge** or **Review**), how long it has waited (**Waiting**),
96−what is at stake (**At stake**), and **Why this one**, such as
97−"Everything it needs is met; it lands when you merge it." When others in
98−its tier are waiting too, it adds that this one has waited longest.
98+**Start here** is the first row of Where you're needed, set apart at the
99+top: the most pressing tier, and within it the row that has waited
100+longest. g1t picks it with that rule, so the same rows always give the
101+same pick. It says why, such as "Everything it needs is met; it lands when
102+you merge it." When others in its tier are waiting too, it adds that this
103+one has waited longest. Beside its button are what is at stake, how long
104+it has waited, and who is on it.
99105
100106 Its title links to what it picked: the pull request, issue or build. The
101−button under it opens the same place. For a pull request (one ready to
107+button opens the same place. For a pull request (one ready to
102108 merge, a review asked of you, or yours that is stuck) it says **Open pull
103109 request**; you merge or review there. For anything else it names the
104110 action, such as **See the build** or **Respond**.
106112 ## Since you were last here
107113
108114 Home remembers when you were last on it, for each workspace, with your
109−account, so it is the same on your laptop and your phone. The second half
110−of the page covers what happened since then, and its heading says the span
115+account, so it is the same on your laptop and your phone. The span's card
116+covers what happened since then, and its heading says the span
111117 in words, such as **Since Tuesday evening · 2 days** or **Since 40
112118 minutes ago**.
113119
149155
150156 ### What changed
151157
152−The first card, **What changed**, has g1t tell you what happened in the
158+**What changed**, under People in the span's card, has g1t tell you what happened in the
153159 span in plain English: what changed, why, and what is live. It is not
154160 written until you ask, since each summary is a call to your workspace's
155161 AI provider:
169175
170176 The question g1t is sent reads as you would ask it, with the span's start
171177 in your time zone: "Summarize what changed since I was last here (2:12
172−PM) in plain English." Choosing another span with the switch starts the
173−card over. When Ask is not set up, the card says so: owners get a link to
178+PM) in plain English." Choosing another span with the switch starts it
179+over. When Ask is not set up, the card says so: owners get a link to
174180 connect a provider and choose it for Ask, and members are told to ask an
175181 owner.
176−
177−### While you were away
178182
179−The next card says what finished by itself in the span, and what is
180−live. (Over **Last 24 hours** and **Last 7 days** it is called **Settled
181−for you**.) Its first line counts:
182−
183−| Count | What it is |
184−| --- | --- |
185−| *4 notifications settled* | Your notifications in this workspace that [settled themselves](/guides/notifications/#threads-that-settle-themselves) in the span: a later run passed, the pull request merged, the deployment went out. One you marked done yourself is not counted |
186−| *3 changes live*, *1 deploying*, *1 waiting to deploy*, *1 deploy failed* | The merges and pushes under [Landed](#landed), by whether each is [live](#live-after-merge) |
187−| *2 things still need you* | The rows of [Where you're needed](#where-youre-needed); it takes you there |
188−
189−Under it, each notification that settled, newest first, with why it
190−settled and who did it, such as **CI passed in run 330** or **Merged by
191−@ana**. **Done** opens the notifications that are done.
192−
193183 ### People
194184
195−The second card, **People**, lists what people did in the span, one line
185+The span's card opens with **People**: what people did in the span, one line
196186 per kind of thing with its count, and each line opens the place that lists
197187 them: a project's Activity tab for pushes, its pull requests or issues,
198188 Artifacts, Chat. Beside the heading are the faces of the five most active,
212202
213203 A push an import made names nobody, so it is counted without a face.
214204
215−The card says when it has nothing to show, and names the part that didn't
216−answer (Code or Chat) so an empty card never reads as a quiet day.
205+It says when it has nothing to show, and names the part that didn't
206+answer (Code or Chat) so an empty list never reads as a quiet day.
217207
218208 ### Landed
219209
220−Everything that landed in the span, newest first:
210+The rest of the span's card is everything that landed in the span,
211+newest first:
221212
222213 | Row | What it is |
223214 | --- | --- |
224215 | A pull request merged | With who wrote it, and who merged it when that was someone else |
225216 | A push to a default branch | One row per push a person made straight to a default branch: its commit's first line when it brought one, else how many commits, and who pushed. For the ten projects pushed to most in the span; past those, one row per project with how many commits and pushes |
226−| A production build that went live | A project's production build that built and was served (previews stay under Deploys) |
217+| A production build | Only one that no merge or push on the list already speaks for: a redeploy, a rollback, a build of a project whose pushes weren't read, or one that failed. A build that shipped a merge is that merge's [live label](#live-after-merge), not a row of its own. Previews stay on the project's Deployments |
227218 | A release published | Its tag and name, and who released it |
228219 | A package version published | Its name and version, and who published it |
229220
230−The card's line counts them by kind. The first
231−five show; **Show more** opens the rest. Over **Last 7 days** the rows are
221+Its line counts them by kind, and how many of the merges and pushes are
222+live, deploying, waiting to deploy, or failed to deploy. The first five
223+show; **Show more** opens the rest. Over **Last 7 days** the rows are
232224 grouped by day (**Today**, **Yesterday**, **Sun, Oct 11**), five of each
233225 day showing.
234226
281273 requests, pushes and releases, deployments) didn't answer, the card says
282274 so under the list.
283275
284−### Deploys
276+## Running now
285277
286−Every build that finished in the span, production and previews: **Went
287−live** when it built and was served (it may have been replaced since), or
288−**Failed**. Skipped builds are left out. A workspace whose projects did not
289−build in the span has no Deploys card.
278+At the top of the side column, what is going right now, longest-going
279+first, each with its state and how long it has gone:
290280
291−### Spent
281+| Kind | What it is |
282+| --- | --- |
283+| Workflow run | A [workflow](/guides/actions/) run that is queued, waiting, running, or waiting for approval, in the ten projects pushed to most in the span (their newest 20 runs each) |
284+| Build | A project's newest build, queued or building |
285+
286+## Spent
292287
293−What the workspace spent over the span, from its
288+In the side column: what the workspace spent over the span, from its
294289 [statement](/guides/usage-and-billing/), with each line at its price: what
295290 was charged plus what your plan's included usage, a trial, a pool or a
296291 discount paid of it. The lines are the statement's kinds of charge, such
304299 yesterday counts all of yesterday. Usage that g1t measures through the
305300 month, such as hosting requests and storage, shows on the day it is added
306301 to the statement.
307−
308−## Running now
309302
310−What is going right now, longest-going first:
303+## Settled for you
311304
312−| Kind | What it is |
313−| --- | --- |
314−| Workflow run | A [workflow](/guides/actions/) run that is queued, waiting, running, or waiting for approval, in the ten projects pushed to most in the span (their newest 20 runs each) |
315−| Build | A project's newest build, queued or building |
305+At the bottom of the side column: your notifications in this workspace
306+that [settled themselves](/guides/notifications/#threads-that-settle-themselves)
307+in the span, newest first, each with why, such as **CI passed in run 330**
308+or **Merged by @ana**: a later run passed, the pull request merged, the
309+deployment went out. One you marked done yourself is not counted. **Done**
310+opens the notifications that are done.
316311
317312 ## While the page is open
318313
349344 repositories.
350345 - Where you're needed has no rows from Code, and no notifications about a
351346 repository.
352−- Landed, Deploys and Running now are not shown.
347+- Landed and Running now are not shown.
353348 - Spent is the same for every member.
354349
355350 ## When something doesn't answer
362357 - People counts what could be read, and names the part that is missing
363358 (Code or Chat). If nothing could be read, the card says so, and the
364359 sentence under the heading says what people did couldn't be read.
365−- Landed, Deploys and Spent each say their part didn't answer. When
360+- Landed and Spent each say their part didn't answer. When
366361 deployments or sessions can't be read, Landed shows its rows without
367362 their live labels or agent markers.
368−- While you were away says when your notifications can't be read.
363+- Settled for you says when your notifications can't be read.
369364 - What changed says when Ask can't be reached or isn't set up, or when
370365 the question could not be sent.
371366 - If your last visit can't be read, the span is the last 24 hours and the
+1−1
122122 a few words, and who did it when someone did: **CI passed in run 330**,
123123 **Merged by @ana**, **Production of site deployed at 8e30fb4**,
124124 **Review no longer asked of you**. A thread you marked done yourself says
125−nothing there. [Home](/guides/home/#while-you-were-away) lists what
125+nothing there. [Home](/guides/home/#settled-for-you) lists what
126126 settled for you since your last visit.
127127
128128 A notification that settled is no longer shown again: tabs you open later
+10−9
1414 import { askG1t, openAsk } from "../lib/ask";
1515 import { HOME_STARTERS, type Span, summarizeQuestion } from "../lib/home";
1616 import { Suggestion, Suggestions } from "./ai-elements/suggestion";
17−import { Card } from "./home";
1817 import { MarkGlyph } from "./logo";
1918 import { Button } from "./ui/button";
2019 import { Hint } from "./ui/hint";
9695 }
9796
9897 /**
99− * What changed: g1t's summary of the span in plain English, on the page.
100− * Asked only when the viewer presses Summarize (each answer is a call to
101− * the workspace's own AI provider); asked again for another span.
98+ * What changed: g1t's summary of the span in plain English, on the page,
99+ * a strip in the span's card. Asked only when the viewer presses
100+ * Summarize (each answer is a call to the workspace's own AI provider);
101+ * asked again for another span.
102102 */
103103 export function WhatChangedCard({ slug, span, tz }: { slug: string; span: AskSpan; tz: string | null }) {
104104 // Each ask is its own: Summarize again asks anew.
125125 const onDone = useCallback((channelId: string | null) => setDone({ channelId }), []);
126126
127127 return (
128− <div ref={card} className="scroll-mt-4">
129− <Card title="What changed" hint={`In plain English, ${label}`}>
130− <div className="px-5 pb-5">
128+ <div ref={card} className="scroll-mt-4 border-t border-line">
129+ <div className="px-5 py-3.5">
131130 {!asked ? (
132131 <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
133− <p className="grow text-sm text-muted">g1t reads the commits, pull requests, deploys and conversations {label} and tells you what changed, why, and what is live.</p>
132+ <p className="grow text-sm text-muted">
133+ <span className="font-medium text-fg">What changed, in plain English.</span> g1t reads the commits, pull requests, deploys and conversations {label} and
134+ tells you what changed, why, and what is live.
135+ </p>
134136 <Button type="button" variant="accent" size="sm" onClick={summarize} onPointerEnter={() => void loadSummary()} className="shrink-0 self-start sm:self-auto">
135137 <Sparkles size={14} />
136138 Summarize
171173 </>
172174 )}
173175 </div>
174− </Card>
175176 </div>
176177 );
177178 }
+125−199
3333 type AttentionKind,
3434 type AttentionRow,
3535 type Away,
36− type DeployRow,
3736 type Digest,
3837 type DigestActor,
3938 type DigestColumn,
204203 );
205204 }
206205
207−/** Everything waiting on you, most pressing and longest waiting first; past the first few, the rest fold away. */
206+/**
207+ * Everything waiting on you, most pressing and longest waiting first, with
208+ * the one g1t picked to start with on top and why; past the first few, the
209+ * rest fold away.
210+ */
208211 export function NeedsList({ attention, now }: { attention: Attention; now: number }) {
209− const { rows, missing } = attention;
210− const arrived = useArrived(rows.map((row) => row.key));
212+ const { start, missing } = attention;
213+ const rows = start ? attention.rows.filter((row) => row.key !== start.row.key) : attention.rows;
214+ const arrived = useArrived(attention.rows.map((row) => row.key));
211215 return (
212216 <>
213− {rows.length === 0 ? (
214− <Quiet>{missing.length > 0 ? "Nothing needs you in what could be read." : "Nothing needs you. Reviews, invitations and messages for you show here as soon as they arrive."}</Quiet>
217+ {start ? (
218+ <StartRow start={start} now={now} arrived={arrived.has(start.row.key)} />
215219 ) : (
216− <ul>
220+ attention.rows.length === 0 && (
221+ <Quiet>{missing.length > 0 ? "Nothing needs you in what could be read." : "Nothing needs you. Reviews, invitations and messages for you show here as soon as they arrive."}</Quiet>
222+ )
223+ )}
224+ {rows.length > 0 && (
225+ <ul className={cn(start && "border-t border-line")}>
217226 {rows.slice(0, ROWS).map((row) => (
218227 <NeedItem key={row.key} row={row} now={now} arrived={arrived.has(row.key)} />
219228 ))}
259268 /** Rows about a pull request: Start here's button opens it, and says so, rather than naming the step (Merge, Review) it can't take there. */
260269 const PULL_KINDS = new Set<AttentionKind>(["ready", "review", "pull_blocked"]);
261270
262−export function StartHere({ attention, now }: { attention: Attention; now: number }) {
263− const start = attention.start;
271+/** The one g1t picked to start with: the first of Where you're needed, a step bigger, with why. */
272+function StartRow({ start, now, arrived }: { start: NonNullable<Attention["start"]>; now: number; arrived?: boolean }) {
273+ const { row } = start;
274+ const owner = row.owner ? (row.owner.agent ? `@${row.owner.name}` : row.owner.name) : null;
264275 return (
265− <Card
266− title={
267− <>
268− Start here <span className="font-normal text-faint">· picked by g1t</span>
269− </>
270− }
271− >
272− {!start ? (
273− <Quiet>{attention.missing.length > 0 ? "Nothing to pick from what could be read." : "Nothing needs you, so there's nothing to pick."}</Quiet>
274− ) : (
275− <div className="px-5 pb-5">
276− <div className="flex items-start gap-3">
277− <span className="mt-0.5 shrink-0">{ICON[start.row.kind]}</span>
278− <div className="min-w-0">
279− <Link to={start.row.action.to} className="text-[0.9375rem] leading-snug font-semibold text-fg hover:underline">
280− {start.row.title}
281− </Link>
282− <p className="mt-1 line-clamp-2 text-xs text-muted">{start.row.detail}</p>
283− </div>
276+ <div className={cn("mx-3 mb-3 rounded-xl border border-accent/25 bg-accent/5 px-4 py-3.5", arrived && ARRIVED)}>
277+ <p className="text-[0.6875rem] font-medium tracking-wide text-accent uppercase">Start here · picked by g1t</p>
278+ <div className="mt-2 flex items-start gap-3">
279+ <span className="mt-0.5 shrink-0">{ICON[row.kind]}</span>
280+ <div className="min-w-0 grow">
281+ <Link to={row.action.to} className="text-[0.9375rem] leading-snug font-semibold text-fg hover:underline">
282+ {row.title}
283+ </Link>
284+ <p className="mt-0.5 line-clamp-2 text-xs text-muted">{row.detail}</p>
285+ <p className="mt-2 text-xs leading-relaxed text-fg-soft">{start.why}</p>
286+ <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2">
287+ <ButtonLink to={row.action.to} variant="accent" size="sm">
288+ {PULL_KINDS.has(row.kind) ? "Open pull request" : row.action.label}
289+ <ArrowRight size={14} />
290+ </ButtonLink>
291+ <span className="flex flex-wrap items-center gap-x-2 text-xs text-faint">
292+ {row.stake && <StakeText stake={row.stake} />}
293+ {row.at > 0 && <span suppressHydrationWarning>waiting {waited(row.at, now)}</span>}
294+ {owner && (
295+ <span className="inline-flex items-center gap-1">
296+ <User size={11} />
297+ {owner}
298+ </span>
299+ )}
300+ </span>
284301 </div>
285− <dl className="mt-4 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-xs">
286− {start.row.owner && (
287− <>
288− <dt className="text-muted">Owner</dt>
289− <dd className="flex min-w-0 items-center gap-1.5 text-fg-soft">
290− <User size={12} className="shrink-0 text-muted" />
291− <span className="truncate">{start.row.owner.agent ? `@${start.row.owner.name}` : start.row.owner.name}</span>
292− </dd>
293− </>
294− )}
295− <dt className="text-muted">Next</dt>
296− <dd className="text-fg-soft">{start.row.action.label}</dd>
297− {start.row.at > 0 && (
298− <>
299− <dt className="text-muted">Waiting</dt>
300− <dd className="text-fg-soft" suppressHydrationWarning>
301− {waited(start.row.at, now)}
302− </dd>
303− </>
304− )}
305− {start.row.stake && (
306− <>
307− <dt className="text-muted">At stake</dt>
308− <dd>
309− <StakeText stake={start.row.stake} />
310− </dd>
311− </>
312− )}
313− </dl>
314− <p className="mt-4 rounded-lg bg-raised/60 px-3 py-2.5 text-xs leading-relaxed text-fg-soft">
315− <span className="font-medium text-fg">Why this one: </span>
316− {start.why}
317− </p>
318− <Link
319− to={start.row.action.to}
320− className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-lg bg-accent px-3 text-sm font-medium text-bg transition-colors hover:bg-accent-hover"
321− >
322− {PULL_KINDS.has(start.row.kind) ? "Open pull request" : start.row.action.label}
323− <ArrowRight size={14} />
324− </Link>
325302 </div>
326− )}
327− </Card>
303+ </div>
304+ </div>
328305 );
329306 }
330307
368345 /** The lines: each what was done, with its count in the words, linked to where it is listed. */
369346 function Lines({ column }: { column: DigestColumn }) {
370347 return (
371− <ul className="mt-3 space-y-1.5">
348+ <ul className="mt-3 grid gap-x-6 gap-y-1.5 sm:grid-cols-2">
372349 {column.lines.map((line) => (
373350 <li key={line.key} className="flex items-start gap-2 text-sm text-fg">
374351 <span className="mt-1.75 size-1.5 shrink-0 rounded-full bg-fg-soft/60" aria-hidden />
386363 }
387364
388365 /**
389− * What people did in the span: lines with counts linked to where each
390− * thing is listed, and the most active faces. It says when it is empty,
391− * and which part didn't answer.
366+ * What people did in the span, the top of the span's card: lines with
367+ * counts linked to where each thing is listed, and the most active faces.
368+ * It says when it is empty, and which part didn't answer.
392369 */
393−export function PeopleCard({ digest, span, code }: { digest: Digest | null; span: Pick<Span, "key" | "note" | "words">; code: boolean }) {
370+export function PeopleSummary({ digest, span, code }: { digest: Digest | null; span: Pick<Span, "key" | "note" | "words">; code: boolean }) {
394371 const when = inSpan(span);
395372 if (!digest) {
396373 return (
397− <section className="rounded-2xl border border-line bg-surface p-5">
374+ <div className="px-5 py-4">
398375 <p className="text-sm text-muted">What happened couldn&apos;t be read right now. It shows here once Code and Chat answer.</p>
399− </section>
376+ </div>
400377 );
401378 }
402379 const silent = digest.missing.filter((name) => name === "Code" || name === "Chat");
403380 const col = digest.people;
404381 return (
405− <section className="rounded-2xl border border-line bg-surface p-5" aria-label="What people did">
382+ <div className="px-5 py-4" aria-label="What people did">
406383 <div className="flex flex-wrap items-center gap-3">
407384 <h3 className="inline-flex items-center gap-1.5 text-sm font-semibold text-fg">
408385 <Users size={15} className="text-muted" />
435412 </p>
436413 )}
437414 {digest.partial && <p className="mt-3 text-xs text-faint">More happened than Home reads at once, so some counts are low.</p>}
438− </section>
415+ </div>
439416 );
440417 }
441418
442419 export function SpanSkeleton() {
443420 return (
444− <section className="space-y-3 rounded-2xl border border-line bg-surface p-5" aria-busy="true">
421+ <div className="space-y-3 px-5 py-4" aria-busy="true">
445422 <div className="flex items-center justify-between">
446423 <Skeleton className="h-3.5 w-20" />
447424 <Skeleton className="h-5 w-24 rounded-full" />
449426 <Skeleton className="h-3.5 w-3/4" />
450427 <Skeleton className="h-3.5 w-1/2" />
451428 <Skeleton className="h-3.5 w-2/3" />
452− </section>
429+ </div>
453430 );
454431 }
455432
574551 function LandedItem({ change, now, tz, timeOnly, arrived }: { change: LandedChange; now: number; tz: string | null; timeOnly?: boolean; arrived?: boolean }) {
575552 return (
576553 <li className={cn(rowClass, "items-start", arrived && ARRIVED)}>
577− <span className="mt-0.5 shrink-0">{LANDED_ICON[change.kind]}</span>
554+ <span className="mt-0.5 shrink-0">{change.failed ? <Rocket size={15} className="shrink-0 text-danger" /> : LANDED_ICON[change.kind]}</span>
578555 <div className="min-w-0 grow">
579556 <div className="flex min-w-0 items-center gap-1.5">
580557 {change.refsIn ? (
600577 );
601578 }
602579
580+/** How many of the span's merges and pushes are live, deploying, waiting or failed: the feed's line. */
581+function shipFacts(landed: LandedChange[]): { key: string; text: string; tone: BadgeTone }[] {
582+ const count = (state: ShipInfo["state"]) => landed.filter((change) => change.shipped?.state === state).length;
583+ const failed = count("failed") + landed.filter((change) => change.failed).length;
584+ const facts: { key: string; text: string; tone: BadgeTone }[] = [];
585+ if (count("live") > 0) facts.push({ key: "live", text: `${count("live").toLocaleString("en-US")} live`, tone: "success" });
586+ if (count("deploying") > 0) facts.push({ key: "deploying", text: `${count("deploying").toLocaleString("en-US")} deploying`, tone: "info" });
587+ if (count("waiting") > 0) facts.push({ key: "waiting", text: `${count("waiting").toLocaleString("en-US")} waiting to deploy`, tone: "neutral" });
588+ if (failed > 0) facts.push({ key: "failed", text: `${failed.toLocaleString("en-US")} deploy failed`, tone: "danger" });
589+ return facts;
590+}
591+
603592 /**
604− * Everything that landed in the span, newest first: pull requests merged,
605− * commits pushed straight to a default branch, production builds that went
606− * live, releases and package versions published. Each merge and push says
593+ * Everything that landed in the span, newest first, in the span's card:
594+ * pull requests merged, commits pushed straight to a default branch,
595+ * releases and package versions published, and production builds no merge
596+ * or push already speaks for (`landedDeploys`). Each merge and push says
607597 * whether it is live yet, and one made in a harness session carries the
608598 * agent marker. Over the last 7 days, day by day.
609599 */
610−export function LandedCard({
600+export function LandedFeed({
611601 landed,
612602 missing,
613603 code,
628618 const arrived = useArrived(landed?.map((change) => change.key) ?? [], scope);
629619 if (!code) return null;
630620 const render = (change: LandedChange) => <LandedItem key={change.key} change={change} now={now} tz={tz} timeOnly={byDay} arrived={arrived.has(change.key)} />;
621+ const facts = landed ? shipFacts(landed) : [];
631622 return (
632− <Card title="Landed" hint={landed && landed.length > 0 ? landedHint(landed) : undefined}>
623+ <div className="border-t border-line">
624+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 px-5 pt-3.5 pb-2">
625+ <h3 className="text-sm font-semibold text-fg">Landed</h3>
626+ {landed && landed.length > 0 && <span className="text-xs text-muted">{landedHint(landed)}</span>}
627+ {facts.length > 0 && (
628+ <ul className="flex flex-wrap gap-1.5 sm:ml-auto" aria-label="Deployed">
629+ {facts.map((fact) => (
630+ <li key={fact.key}>
631+ <Badge tone={fact.tone}>{fact.text}</Badge>
632+ </li>
633+ ))}
634+ </ul>
635+ )}
636+ </div>
633637 {!landed ? (
634638 <Quiet>Code didn&apos;t answer, so what landed can&apos;t be listed right now.</Quiet>
635639 ) : landed.length === 0 ? (
636640 <Quiet>{missing.length > 0 ? "Nothing landed in what could be read." : "Nothing merged, pushed to a default branch, deployed to production or published in this span."}</Quiet>
637641 ) : byDay ? (
638642 landedByDay(landed, now, tz).map((day) => (
639− <section key={day.key} aria-label={day.label} className="border-t border-line first-of-type:border-t-0">
640− <h3 className="flex items-baseline gap-2 bg-raised/40 px-5 py-1.5 text-xs font-medium text-fg-soft">
643+ <section key={day.key} aria-label={day.label} className="border-t border-line">
644+ <h4 className="flex items-baseline gap-2 bg-raised/40 px-5 py-1.5 text-xs font-medium text-fg-soft">
641645 {day.label}
642646 <span className="font-normal text-faint tabular-nums">{day.rows.length.toLocaleString("en-US")}</span>
643− </h3>
647+ </h4>
644648 <Folded items={day.rows} render={render} />
645649 </section>
646650 ))
647651 ) : (
648− <Folded items={landed} render={render} />
652+ <div className="border-t border-line">
653+ <Folded items={landed} render={render} />
654+ </div>
649655 )}
650656 {landed && <Missing names={missing} what="everything that landed" />}
651− </Card>
657+ </div>
652658 );
653659 }
654660
655−// --- While you were away -----------------------------------------------------------
661+// --- Settled for you ---------------------------------------------------------------
656662
657663 /**
658− * What g1t settled for you since you were last here (a later run passed,
659− * it merged, it deployed), what shipped, and what still needs you: the
660− * counts on one line, then each thing that settled and why.
664+ * Your notifications that settled by themselves in the span (a later run
665+ * passed, it merged, it deployed), each with why: in the side column.
666+ * What shipped is the Landed feed's; what still needs you, Where you're
667+ * needed.
661668 */
662−export function AwayCard({ away, span, now }: { away: Away | null; span: Pick<Span, "key" | "note" | "words">; now: number }) {
663− const title = span.key === "last" && span.note == null ? "While you were away" : "Settled for you";
664− if (!away) {
665− return (
666− <Card title={title}>
669+export function SettledCard({ away, now }: { away: Away | null; now: number }) {
670+ const count = away?.settled.length ?? 0;
671+ return (
672+ <Card title="Settled for you" hint={away ? (count > 0 ? `${count.toLocaleString("en-US")} finished by themselves` : "Nothing finished by itself") : undefined} link={{ label: "Done", to: "/notifications/done" }}>
673+ {!away ? (
667674 <Quiet>Your notifications couldn&apos;t be read, so what settled can&apos;t be listed right now.</Quiet>
668− </Card>
669− );
670− }
671− const facts: { key: string; text: string; tone: BadgeTone; to: string | null }[] = [];
672− const plural = (n: number, one: string, many = `${one}s`) => `${n.toLocaleString("en-US")} ${n === 1 ? one : many}`;
673− facts.push({ key: "settled", text: `${plural(away.settled.length, "notification")} settled`, tone: away.settled.length > 0 ? "success" : "neutral", to: "/notifications?view=done" });
674− if (away.shipped > 0) facts.push({ key: "shipped", text: `${plural(away.shipped, "change")} live`, tone: "success", to: null });
675− if (away.deploying > 0) facts.push({ key: "deploying", text: `${away.deploying.toLocaleString("en-US")} deploying`, tone: "info", to: null });
676− if (away.waiting > 0) facts.push({ key: "waiting", text: `${away.waiting.toLocaleString("en-US")} waiting to deploy`, tone: "neutral", to: null });
677− if (away.failed > 0) facts.push({ key: "failed", text: `${away.failed.toLocaleString("en-US")} deploy failed`, tone: "danger", to: null });
678− facts.push({ key: "needs", text: away.needs > 0 ? `${plural(away.needs, "thing")} still ${away.needs === 1 ? "needs" : "need"} you` : "Nothing needs you", tone: away.needs > 0 ? "warn" : "neutral", to: "#needed" });
679− return (
680− <Card title={title} hint="What finished by itself, and what is live" link={{ label: "Done", to: "/notifications?view=done" }}>
681− <ul className="flex flex-wrap gap-1.5 px-5 pb-3" aria-label="In short">
682− {facts.map((fact) => (
683− <li key={fact.key}>
684− {fact.to ? (
685− <Link to={fact.to} className="rounded-full focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none">
686− <Badge tone={fact.tone} size="md">
687− {fact.text}
688− </Badge>
689− </Link>
690− ) : (
691− <Badge tone={fact.tone} size="md">
692− {fact.text}
693− </Badge>
694− )}
695− </li>
696− ))}
697− </ul>
698− {away.settled.length > 0 && (
675+ ) : count === 0 ? null : (
699676 <div className="border-t border-line">
700677 <Folded
701678 items={away.settled}
702679 render={(row) => (
703− <li key={row.key} className={rowClass}>
704− <CheckCheck size={15} className="shrink-0 text-success" aria-hidden="true" />
680+ <li key={row.key} className={cn(rowClass, "items-start px-4")}>
681+ <CheckCheck size={14} className="mt-0.5 shrink-0 text-success" aria-hidden="true" />
705682 <div className="min-w-0 grow">
706683 <Link to={row.to} className="block truncate text-sm text-fg hover:underline">
707684 {row.title}
712689 </p>
713690 </div>
714691 <span className="shrink-0 text-xs text-faint tabular-nums" suppressHydrationWarning>
715− {waited(row.at, now)} ago
692+ {waited(row.at, now)}
716693 </span>
717694 </li>
718695 )}
723700 );
724701 }
725702
726−export function DeploysCard({
727− deploys,
728− complete,
729− code,
730− now,
731− tz,
732− scope,
733−}: {
734− deploys: DeployRow[] | null;
735− complete: boolean;
736− code: boolean;
737− now: number;
738− tz: string | null;
739− scope?: string;
740−}) {
741− const arrived = useArrived(deploys?.map((row) => row.key) ?? [], scope);
742− // A workspace that doesn't deploy has nothing to say here.
743− if (!code || (deploys && deploys.length === 0)) return null;
744− const failed = deploys?.filter((row) => row.outcome === "failed").length ?? 0;
745− return (
746− <Card title="Deploys" hint={deploys ? `${deploys.length.toLocaleString("en-US")} finished${failed ? ` · ${failed} failed` : ""}` : undefined}>
747− {!deploys ? (
748− <Quiet>Deployments didn&apos;t answer, so builds can&apos;t be listed right now.</Quiet>
749− ) : (
750− <Folded
751− items={deploys}
752− render={(row) => (
753− <li key={row.key} className={cn(rowClass, arrived.has(row.key) && ARRIVED)}>
754− <Rocket size={15} className={cn("shrink-0", row.outcome === "failed" ? "text-danger" : "text-success")} />
755− <div className="min-w-0 grow">
756− <Link to={row.to} className="block truncate text-sm text-fg hover:underline">
757− {row.kind === "production" ? "Production" : `Preview of ${row.branch ?? "a branch"}`} of {row.project}
758− <span className={cn("ml-2 text-xs font-medium", row.outcome === "failed" ? "text-danger" : "text-success")}>
759− {row.outcome === "failed" ? "Failed" : "Went live"}
760− </span>
761− </Link>
762− <p className="truncate text-xs text-muted">
763− <span className="font-mono">{row.commit}</span> · by {row.by}
764− </p>
765− </div>
766− <When at={row.at} now={now} tz={tz} />
767− </li>
768− )}
769− />
770− )}
771− {deploys && !complete && <p className="border-t border-line px-5 py-2.5 text-xs text-faint">Some projects&apos; builds couldn&apos;t be read.</p>}
772− </Card>
773− );
774−}
775−
776703 // --- Spend in the span ------------------------------------------------------------
777704
778705 /**
795722 <Quiet>Billing didn&apos;t answer, so spend can&apos;t be shown right now.</Quiet>
796723 ) : (
797724 <div className="px-5 pb-5">
798− <div className="text-3xl font-semibold tracking-tight text-fg tabular-nums">{money(spend.totalMicros)}</div>
725+ <div className="text-2xl font-semibold tracking-tight text-fg tabular-nums">{money(spend.totalMicros)}</div>
799726 <p className="mt-1 text-xs text-muted">
800727 {spend.from === spend.to ? utcDay(spend.from) : `${utcDay(spend.from)} – ${utcDay(spend.to)}`}
801728 {spend.monthMicros != null && ` · ${money(spend.monthMicros)} this month so far`}
820747 </ul>
821748 </>
822749 )}
823− <p className="mt-4 text-[0.6875rem] leading-relaxed text-faint">
824− At price, as your statement shows it. The statement keeps whole days, midnight to midnight UTC, so this counts from the start of the day the span began.
825− </p>
750+ <p className="mt-4 text-[0.6875rem] leading-relaxed text-faint">At price, by whole UTC days from the day the span began.</p>
826751 </div>
827752 )}
828753 </Card>
857782 <Folded
858783 items={rows}
859784 render={(row) => (
860− <li key={row.key} className={cn(rowClass, arrived.has(row.key) && ARRIVED)}>
861− <span className="shrink-0">{RUNNING_ICON[row.kind]}</span>
785+ // In the side column: the title gets the width, its state and how long under it.
786+ <li key={row.key} className={cn(rowClass, "items-start px-4", arrived.has(row.key) && ARRIVED)}>
787+ <span className="mt-0.5 shrink-0">{RUNNING_ICON[row.kind]}</span>
862788 <div className="min-w-0 grow">
863− <Link to={row.to} className="block truncate text-sm text-fg hover:underline">
789+ <Link to={row.to} className="line-clamp-2 text-sm text-fg hover:underline">
864790 {row.title}
865791 </Link>
866792 <p className="truncate text-xs text-muted">{row.detail}</p>
793+ <p className="mt-1 flex items-center gap-1.5 text-xs text-fg-soft">
794+ <Loader size={11} className="animate-spin text-accent motion-reduce:animate-none" aria-hidden />
795+ {row.status}
796+ <span className="text-faint tabular-nums" suppressHydrationWarning>
797+ · {waited(row.at, now)}
798+ </span>
799+ </p>
867800 </div>
868− <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-raised px-2 py-0.5 text-xs text-fg-soft">
869− <Loader size={11} className="animate-spin text-accent motion-reduce:animate-none" aria-hidden />
870− {row.status}
871− </span>
872− <span className="hidden w-14 shrink-0 text-right text-xs text-faint tabular-nums sm:inline" suppressHydrationWarning>
873− {waited(row.at, now)}
874− </span>
875801 </li>
876802 )}
877803 />
+17−2
77 type AttentionRow,
88 type CodeNeed,
99 type CodePull,
10+ type DeployRow,
1011 type LandedChange,
1112 actorKind,
1213 agentLabel,
514515 { key: "deploy:site:d2", project: "site", kind: "preview", outcome: "live", commit: "abcdef2", branch: "fix", at: NOW - HOUR, to: "/acme/site/deployments/d2", by: "bo" },
515516 { key: "deploy:site:d3", project: "site", kind: "production", outcome: "failed", commit: "abcdef3", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d3", by: "bo" },
516517 ]);
517− assert.deepEqual(builds.map((change) => [change.kind, change.title]), [["deploy", "Production of site went live"]]);
518− const all = landed([fromActivity, builds]);
519518 assert.deepEqual(
519+ builds.map((change) => [change.kind, change.title, change.failed ?? false]),
520+ [
521+ ["deploy", "Production of site went live", false],
522+ ["deploy", "Production of site failed to deploy", true],
523+ ],
524+ );
525+ const all = landed([fromActivity, builds.slice(0, 1)]);
526+ assert.deepEqual(
520527 all.map((change) => change.key),
521528 ["push:rep_cli", "landed:deploy:site:d1", "package:rep_api:container:api:1.4.0", "push:rep_web", "release:rep_web:v1.4.0", "push:rep_api"],
522529 );
523530 });
524531
532+test("a production build is its own row only where no merge or push on the list says it", () => {
533+ const row = (key: string, commit: string): DeployRow => ({ key, project: "site", kind: "production", outcome: "live", commit, branch: null, at: NOW - HOUR, to: `/acme/site/deployments/${key}`, by: "bo" });
534+ const merge: LandedChange = { key: "merge:1", kind: "merge", title: "Fix", detail: "", to: "/acme/site/pull/1", at: NOW - 2 * HOUR, sha: "aaaaaaa111", repoId: "r" };
535+ const push: LandedChange = { key: "push:1", kind: "push", title: "Push", detail: "", to: "/acme/site/activity", at: NOW - 2 * HOUR, sha: "bbbbbbb222", repoId: "r", shipped: { state: "live", at: NOW - HOUR, to: "/acme/site/deployments/d2" } };
536+ const builds = landedDeploys([row("d1", "aaaaaaa"), row("d2", "ccccccc"), row("d3", "ddddddd")], [merge, push]);
537+ assert.deepEqual(builds.map((change) => change.key), ["landed:d3"]);
538+});
539+
525540 const deploy = (over: Partial<Deployment>): Deployment => ({
526541 id: "d1",
527542 kind: "production",
+17−4
525525 agent?: AgentMark | null;
526526 /** A title that is a commit's subject: its repository (`owner/name`), whose issues its `#12`s are. */
527527 refsIn?: string;
528+ /** A production build that failed (`landedDeploys`). */
529+ failed?: boolean;
528530 };
529531
530532 /** Whether a change is live: shipped by a production deployment, on its way, waiting, or the deployment after it failed. */
617619 return rows;
618620 }
619621
620−/** Production builds that went live in the span, as landed changes; previews stay in Deploys. */
621−export function landedDeploys(deploys: DeployRow[]): LandedChange[] {
622+/**
623+ * Production builds of the span as landed changes, only where nothing on
624+ * the list already says so: a merge or push whose live label links to the
625+ * build (`shipped.to`), or whose commit it built. So a build that shipped
626+ * a merge is that merge's "Live" label, not a row of its own; a redeploy,
627+ * a rollback or a build of a project whose pushes weren't read is a row,
628+ * and so is one that failed (`failed`). Previews stay on their project's
629+ * Deployments.
630+ */
631+export function landedDeploys(deploys: DeployRow[], shown: readonly LandedChange[] = []): LandedChange[] {
632+ const said = (row: DeployRow) =>
633+ shown.some((change) => (change.shipped?.to != null && change.shipped.to === row.to) || (!!change.sha && !!row.commit && change.sha.startsWith(row.commit)));
622634 return deploys
623− .filter((row) => row.kind === "production" && row.outcome === "live")
635+ .filter((row) => row.kind === "production" && !said(row))
624636 .map((row) => ({
625637 key: `landed:${row.key}`,
626638 kind: "deploy" as const,
627− title: `Production of ${row.project} went live`,
639+ title: row.outcome === "failed" ? `Production of ${row.project} failed to deploy` : `Production of ${row.project} went live`,
628640 detail: `${row.commit} · by ${row.by ?? "someone"}`,
629641 to: row.to,
630642 at: row.at,
643+ ...(row.outcome === "failed" ? { failed: true } : {}),
631644 }));
632645 }
633646
+73−76
55
66 import type { Route } from "./+types/home";
77 import {
8− AwayCard,
98 Card,
10− DeploysCard,
11− LandedCard,
9+ LandedFeed,
1210 NeedsList,
13− PeopleCard,
11+ PeopleSummary,
1412 RowsSkeleton,
1513 RunningList,
14+ SettledCard,
1615 SpanSkeleton,
1716 SpendCard,
1817 SpendSkeleton,
19− StartHere,
2018 WindowSwitch,
2119 } from "../../components/home";
2220 import { HomeAsk, WhatChangedCard } from "../../components/home-ask";
154152 const named = (key: string) => (key ? (names[key] ?? "someone") : "an import");
155153 const each = pushes ? pushRows(pushes.pushes.map((push) => ({ ...push, by: named(push.actorKey) })), pushes.history, span) : [];
156154 const fromActivity = landedFromActivity(activity, repoRefs(owned), names, slug).filter((row) => !(row.kind === "push" && pushes?.history[row.key.slice("push:".length)]));
157− const all = merges == null && activity == null ? null : landed([merges ?? [], each, fromActivity, landedDeploys(deployRows ?? [])]);
155+ const all = merges == null && activity == null ? null : landed([merges ?? [], each, fromActivity]);
158156 // Whether each merge and push is live, and which pull requests a harness session made: read together.
159157 const [shipped, sessions] = all
160158 ? await Promise.all([
162160 codeWork && codeWork !== "no_access" ? loadPullSessions(viewer, codeWork.pulls.filter((pull) => pull.status === "merged" && within(pull.mergedAt, span))).catch(() => null) : null,
163161 ])
164162 : [null, null];
163+ // Production builds only where no merge or push already says it went live (or failed).
164+ const told = all ? withSessions(withShipped(all, shipped), sessions) : null;
165165 return {
166166 // What landed: merges, direct pushes, releases and packages, production builds; null when nothing of Code could be read.
167− landed: all ? withSessions(withShipped(all, shipped), sessions) : null,
167+ landed: told ? landed([told, landedDeploys(deployRows ?? [], told)]) : null,
168168 landedMissing: [
169169 ...(code && merges == null ? ["Pull requests"] : []),
170170 ...(code && activity == null ? ["Pushes and releases"] : []),
171171 ...(code && deployRows == null ? ["Deployments"] : []),
172172 ],
173− deploys: deployRows,
174− deploysComplete: deploys?.complete ?? true,
175173 };
176174 })
177175 .catch(() => null);
291289 </p>
292290 </header>
293291
294− {/* 1. Where you're needed now: no window, first and biggest. Start here beside it on wide screens, under it on phones. */}
295− <div className="grid items-start gap-5 lg:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]">
296− <div id="needed" className="scroll-mt-4 lg:col-start-1 lg:row-start-1">
292+ {/*
293+ Two columns on wide screens: what to do and what happened on the
294+ left, read top to bottom; what is going on now and what it costs on
295+ the right, kept in view. On a phone it is one column: Where you're
296+ needed, Running now, what happened, then Spent and Settled (the side
297+ column's cards join it, `contents`, each in its place by `order`).
298+ */}
299+ <div className="grid items-start gap-5 lg:grid-cols-[minmax(0,1fr)_20rem]">
300+ {/* 1. Where you're needed now, with the one to start with on top: no window. */}
301+ <div id="needed" className="order-1 scroll-mt-4 lg:col-start-1 lg:row-start-1">
297302 <Card
298303 title="Where you're needed"
299304 hint="Everything waiting on you, however long ago it started"
304309 </Suspense>
305310 </Card>
306311 </div>
307− <div className="lg:col-start-2 lg:row-start-1">
308− <Suspense
309− fallback={
310− <Card title="Start here">
311− <RowsSkeleton rows={1} />
312− </Card>
313− }
314− >
315− <Await resolve={waiting}>{(att) => <StartHere attention={att} now={now} />}</Await>
316− </Suspense>
317− </div>
318− </div>
319312
320− {/* 2. What happened in the span. */}
321− <section aria-labelledby="since" className="space-y-4">
322− <div className="flex flex-wrap items-center gap-x-3 gap-y-2 pt-2">
323− <div className="flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1">
324− <h2 id="since" className="text-lg font-semibold tracking-tight text-fg">
325− {span.key === "last" && span.note == null ? "Since you were last here" : span.words}
326− </h2>
327− <p className="text-sm text-muted">
328− {span.key === "last" && span.note == null ? `${span.words} · ${span.length}` : spanNote(span.note)}
329− </p>
330− </div>
331− <div className="sm:ml-auto">
332− <WindowSwitch span={span} />
333− </div>
334− </div>
335− <WhatChangedCard key={span.key} slug={slug} span={span} tz={tz} />
336− <Suspense
337− fallback={
338− <Card title={span.key === "last" && span.note == null ? "While you were away" : "Settled for you"}>
339− <RowsSkeleton rows={2} />
340− </Card>
341− }
342− >
343− <Await resolve={away}>{(value) => <AwayCard away={value} span={span} now={now} />}</Await>
344− </Suspense>
345− <Suspense fallback={<SpanSkeleton />}>
346− <Await resolve={digest}>{(value) => <PeopleCard digest={value} span={span} code={loaderData.code} />}</Await>
347− </Suspense>
348− <div className="grid items-start gap-5 lg:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]">
349− <div className="space-y-5">
313+ {/* 2. Now: what is running, what it cost, what settled by itself. */}
314+ <aside aria-label="Now" className="contents lg:sticky lg:top-4 lg:col-start-2 lg:row-span-2 lg:row-start-1 lg:block lg:space-y-5">
315+ <Card title="Running now" className="order-2">
316+ <Suspense fallback={<RowsSkeleton rows={2} />}>
317+ <Await resolve={runningNow}>{(value) => <RunningList rows={value.rows} missing={value.missing} now={now} />}</Await>
318+ </Suspense>
319+ </Card>
320+ <div className="order-4">
350321 <Suspense
351322 fallback={
352− <Card title="Landed">
353− <RowsSkeleton rows={3} />
323+ <Card title="Spent">
324+ <SpendSkeleton />
354325 </Card>
355326 }
356327 >
357− <Await resolve={happened}>
358− {(value) => <LandedCard landed={value?.landed ?? null} missing={value?.landedMissing ?? []} code={loaderData.code} now={now} tz={tz} byDay={span.key === "7d"} scope={span.key} />}
359− </Await>
360− </Suspense>
361− <Suspense fallback={null}>
362− <Await resolve={happened}>
363− {(value) => <DeploysCard deploys={value?.deploys ?? null} complete={value?.deploysComplete ?? true} code={loaderData.code} now={now} tz={tz} scope={span.key} />}
364− </Await>
328+ <Await resolve={spend}>{(value) => <SpendCard spend={value} slug={slug} span={span} />}</Await>
365329 </Suspense>
366330 </div>
367− <div className="space-y-5">
331+ <div className="order-5">
368332 <Suspense
369333 fallback={
370− <Card title="Spent">
371− <SpendSkeleton />
334+ <Card title="Settled for you">
335+ <RowsSkeleton rows={1} />
372336 </Card>
373337 }
374338 >
375− <Await resolve={spend}>{(value) => <SpendCard spend={value} slug={slug} span={span} />}</Await>
339+ <Await resolve={away}>{(value) => <SettledCard away={value} now={now} />}</Await>
376340 </Suspense>
377341 </div>
378− </div>
379− </section>
342+ </aside>
380343
381− {/* 3. Running now. */}
382− <Card title="Running now">
383− <Suspense fallback={<RowsSkeleton rows={2} />}>
384− <Await resolve={runningNow}>{(value) => <RunningList rows={value.rows} missing={value.missing} now={now} />}</Await>
385− </Suspense>
386− </Card>
344+ {/* 3. What happened in the span: who did what, what changed in plain English, what landed. */}
345+ <section aria-labelledby="since" className="order-3 rounded-2xl border border-line bg-surface lg:col-start-1 lg:row-start-2">
346+ <header className="flex flex-wrap items-center gap-x-3 gap-y-2 px-5 pt-4">
347+ <div className="flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1">
348+ <h2 id="since" className="text-base font-semibold tracking-tight text-fg">
349+ {span.key === "last" && span.note == null ? "Since you were last here" : span.words}
350+ </h2>
351+ <p className="text-xs text-muted">{span.key === "last" && span.note == null ? `${span.words} · ${span.length}` : spanNote(span.note)}</p>
352+ </div>
353+ <div className="sm:ml-auto">
354+ <WindowSwitch span={span} />
355+ </div>
356+ </header>
357+ <Suspense fallback={<SpanSkeleton />}>
358+ <Await resolve={digest}>{(value) => <PeopleSummary digest={value} span={span} code={loaderData.code} />}</Await>
359+ </Suspense>
360+ <WhatChangedCard key={span.key} slug={slug} span={span} tz={tz} />
361+ <Suspense
362+ fallback={
363+ <div className="border-t border-line pt-4">
364+ <RowsSkeleton rows={3} />
365+ </div>
366+ }
367+ >
368+ <Await resolve={happened}>
369+ {(value) => (
370+ <LandedFeed
371+ landed={value?.landed ?? null}
372+ missing={value?.landedMissing ?? []}
373+ code={loaderData.code}
374+ now={now}
375+ tz={tz}
376+ byDay={span.key === "7d"}
377+ scope={span.key}
378+ />
379+ )}
380+ </Await>
381+ </Suspense>
382+ </section>
383+ </div>
387384 </div>
388385 );
389386 }

Comments

Sign in to comment.