Public: anyone can read this; the crew can change it.

Closed deals from earlier years fold into one line per year so a long book stays quick to read #321

opened this

Check · Review · Land · Live — Its check is red. Next: builder, its check is red.

Ready to land

mason/deal-clock:mason/312-closed-deals-from-earlier-years-fold-int → main 2 commits fork

  • Ready to merge — no conflicts with main. 2 commits will land on main.

    Check faileddeploy a09a542

    Website spec: 3 findings, 3 the edge owns (report only)
    • / no <link rel="canonical"> — canonical
    • / missing og:image — open-graph
    • / http://127.0.0.1:44853/tokens.css?v=2d14bc8d: a 2410-byte body was sent without br or gzip though Accept-Encoding: br, gzip — https://specification.website/ (the edge owns this)
    • / http://127.0.0.1:44853/static/app.css?v=86b77628: a 23667-byte body was sent without br or gzip though Accept-Encoding: br, gzip — https://specification.website/ (the edge owns this)
    • / no Cross-Origin-Opener-Policy — coop (the edge owns this)
    • / no ETag on the HTML — caching
    You need write access to merge.

Review

Sign in to review this change.
7 files changed +128 −2
README.md +1 −1
@@ -200,7 +200,7 @@ deals and the commission earned on deals closed this year. Commission is yours
200200 alone: it never appears on the client timeline, a handout or a reminder.
201201 The Closed list shows each closed deal's price and "Closed in N days" (the
202202 contract date to the day its Closing date was marked done; deals that fell
203 through are listed apart and left out). Under the book line, the year's pace
203 through are listed apart and left out). Closed and fell-through deals from earlier years fold into one line per year ("2025 · 14 closed · $5,120,000 · commission $128,000"); tap a year to list its deals, and Find opens every year that has a match. Under the book line, the year's pace
204204 reads "Average 34 days to close · fastest 21 days · slowest 52 days" over deals
205205 closed this year, or says in words that none has closed yet. **Print broker
206206 file** on a closed deal makes the one page for the brokerage's transaction file: price,
docs/TECHNICAL.md +2
@@ -182,6 +182,8 @@ deal (`b` goes back). Its print rules in `static/app.css` apply to that page onl
182182
183183 The clock also shows the year's pace under the book line, and each Closed deal's "Closed in N days": days from the contract date to the day the Closing date was marked done, averaged over deals closed this year with the fastest and slowest (`src/pace.ts`, pure). Fell-through and open deals have no pace.
184184
185 Closed and fell-through deals from earlier years fold into one `<details>` per year on the clock (`src/years.ts`, pure): a deal belongs to the year of `closedOn` (or `fellOn`), in the agent's time zone. A narrowed clock (Find, side, closing) opens every year it still lists.
186
185187 A deal is **closed** when its Closing date is marked done (derived, no column; `closed` on `GET /api/deals` and `list_deals`). It leaves the clock for the Closed card, sends no reminders (`/tick`, `reminders_today`, the terminal `r` list), and the terminal date list shows its dates only after `h`. Reopening the Closing date reopens the deal.
186188
187189 **One clock per house.** `planFor` (`src/deals.ts`) refuses to open a deal at an address that already has an open one of the same author, comparing after trimming, collapsing spaces, ignoring case and a trailing period (`sameAddress`). A closed deal (see above) does not count, and the sample deals are another author's. The refusal is a `conflict` Result naming the deal (`existing`): the API and MCP answer 409 `{error: "conflict", message, existing_id}`; the New deal form re-renders with a link to that deal, everything typed kept, and an "Open a second deal anyway" checkbox (`open_anyway`); the terminal asks `yes` to go on. Send `openAnyway: true` (API/MCP: `open_anyway`) to open the second deal. The date preview shows the same notice. `insertDeal` is unguarded, so Restore the sample deals is unaffected. No new contract cell.
docs/design/areas/clock.md +2
@@ -7,3 +7,5 @@ New classes:
77 - `.progress` — the paragraph that holds both at the top of a deal's page.
88
99 - `.closing-clash` — one amber line (`--color-soon`) under a closing on This week, the print sheet and the deal page: "Also closing at 2:30 pm: 2210 Juniper Court", the address linked to its deal.
10
11 - `.closed-year` — one `<details>` per earlier year in the Closed list, its summary a single line ("2025 · 14 closed · $5,120,000 · commission $128,000") with a 2.75rem tap height; opened, it holds the same `.closed` list as this year's deals.
src/pages/clock.ts +17 −1
@@ -28,6 +28,7 @@ import { memoizer, print } from "../memo.ts";
2828 import { bookLine, tally } from "../book.ts";
2929 import { closedInLine, daysToClose, paceLine, paceOf } from "../pace.ts";
3030 import { dollars } from "../money.ts";
31 import { byYear, yearLine } from "../years.ts";
3132 import { progressLine } from "./progress.ts";
3233
3334 /** The days the clock shows: a week behind today (further, if something open is older), to a few days past the last closing. */
@@ -149,6 +150,16 @@ function reminderRows(deal: DealView, on: string, agent: Agent): { count: number
149150 });
150151 }
151152
153 /** The Closed list's rows: closed deals with their price and pace, then the ones that fell through. */
154 const closedList = (deals: readonly (DealView & { fell: boolean })[], on: string): string =>
155 `<ul class="closed">${deals
156 .map((deal) =>
157 deal.fell
158 ? `<li><a href="/deals/${deal.id}">${escape(deal.address)}</a> <b>Fell through</b> <span class="muted">${escape(spoken(fellOn(deal)))} · ${escape(deal.fell_reason ?? "")}</span> ${sideLabel(deal.side)}</li>`
159 : `<li><a href="/deals/${deal.id}">${escape(deal.address)}</a> <span class="muted">closed ${escape(spoken(deal.closing, on))}${deal.sale_price === null ? "" : ` · ${dollars(deal.sale_price)}`}${paced(deal)}</span> ${sideLabel(deal.side)}</li>`,
160 )
161 .join("")}</ul>`;
162
152163 const pages = memoizer<string>(3);
153164
154165 /** The clock, rendered once while nothing it shows has changed. */
@@ -228,13 +239,18 @@ function render(input: {
228239 <ol class="lanes">${deals.map((deal) => lane(deal, start, on)).join("")}</ol>
229240 </div>
230241 </div>`;
242 const narrowed = q !== "" || side !== "" || closing !== "";
243 const { current, earlier } = byYear(
244 [...closed.map((deal) => ({ day: closedOn(deal), fell: false, ...deal })), ...fell.map((deal) => ({ day: fellOn(deal), fell: true, ...deal }))],
245 on,
246 );
231247 const closedCard =
232248 closed.length === 0 && fell.length === 0
233249 ? ""
234250 : `<section id="closed" class="card" data-live="deal.changed">
235251 <h2>Closed <span class="muted">· ${closed.length + fell.length}</span></h2>
236252 ${who.role === "anon" ? "" : `<p class="small"><a href="/deals/csv?closed=1" download>Download spreadsheet with closed deals</a></p>`}
237 <ul class="closed">${closed.map((deal) => `<li><a href="/deals/${deal.id}">${escape(deal.address)}</a> <span class="muted">closed ${escape(spoken(deal.closing, on))}${deal.sale_price === null ? "" : ` · ${dollars(deal.sale_price)}`}${paced(deal)}</span> ${sideLabel(deal.side)}</li>`).join("")}${fell.map((deal) => `<li><a href="/deals/${deal.id}">${escape(deal.address)}</a> <b>Fell through</b> <span class="muted">${escape(spoken(fellOn(deal)))} · ${escape(deal.fell_reason ?? "")}</span> ${sideLabel(deal.side)}</li>`).join("")}</ul>
253 ${current.length === 0 ? "" : closedList(current, on)}${earlier.map((y) => `<details class="closed-year"${narrowed ? " open" : ""}><summary>${escape(yearLine(y))}</summary>${closedList(y.deals, on)}</details>`).join("")}
238254 </section>`;
239255 const restoreCard =
240256 !mayRestore(who).ok
src/years.test.ts added +36
@@ -0,0 +1,36 @@
1 import { expect, test } from "bun:test";
2 import { byYear, yearLine, type Settled } from "./years.ts";
3 import { clock } from "./pages/clock.ts";
4
5 const d = (day: string, price: number | null = null, rate: number | null = null, fell = false): Settled => ({ day, fell, sale_price: price, commission_rate: rate });
6
7 test("this year lists as is; earlier years fold, newest first, at the year boundary", () => {
8 const deals = [d("2026-01-01"), d("2025-12-31", 400000, 250), d("2025-03-01", 600000, 300), d("2024-06-01"), d("2025-05-05", null, null, true)];
9 const { current, earlier } = byYear(deals, "2026-11-11");
10 expect(current).toEqual([deals[0]!]);
11 expect(earlier.map((y) => y.year)).toEqual(["2025", "2024"]);
12 expect(earlier[0]!.deals.length).toBe(3);
13 expect(yearLine(earlier[0]!)).toBe("2025 · 2 closed · $1,000,000 · commission $28,000 · 1 fell through");
14 expect(yearLine(earlier[1]!)).toBe("2024 · 1 closed");
15 });
16
17 test("figures appear only for deals that carry them", () => {
18 const { earlier } = byYear([d("2023-02-02", 500000)], "2026-01-01");
19 expect(yearLine(earlier[0]!)).toBe("2023 · 1 closed · $500,000");
20 });
21
22 const deal = (id: number, address: string, closing: string) => ({
23 id, address, closing, closed: true, fell_through: false, fell_at: null, fell_reason: null, sale_price: 300000, commission_rate: null,
24 contract_date: "2024-01-01", side: "buyer", zone: "UTC", deadlines: [{ kind: "closing", done_at: `${closing}T12:00:00Z`, due: closing }], conflicts: [], closingConflicts: [], parties: [], sent: [], notes: [],
25 }) as never;
26
27 test("the clock folds earlier years, and Find opens a year with a match", () => {
28 const deals = [deal(1, "1 New St", "2026-02-02"), deal(2, "2 Old Rd", "2024-05-05")];
29 const page = (q: string) => clock({ who: { role: "user", name: "a", id: "u" } as never, deals, on: "2026-11-11", q });
30 const plain = page("");
31 expect(plain).toContain("1 New St");
32 expect(plain).toMatch(/<details class="closed-year">\s*<summary>2024 · 1 closed · \$300,000<\/summary>/);
33 const found = page("Old");
34 expect(found).toMatch(/<details class="closed-year" open>/);
35 expect(found).toContain("2 Old Rd");
36 });
src/years.ts added +59
@@ -0,0 +1,59 @@
1 /**
2 * Closed deals by year: this year's list as it is, each earlier year folded
3 * into one line. Plain data in, plain data out. A deal belongs to the year of
4 * the day its Closing date was marked done (a fell-through deal, the day it
5 * fell), both already in the agent's time zone.
6 */
7
8 import { commission, dollars } from "./money.ts";
9
10 /** What folding needs of a closed or fell-through deal. */
11 export type Settled = { day: string; fell: boolean; sale_price: number | null; commission_rate: number | null };
12
13 export type Year<T> = {
14 year: string;
15 deals: T[];
16 closed: number;
17 fell: number;
18 /** Prices of the closed deals added; null when none carries a price. */
19 volume: number | null;
20 /** Commissions of the closed deals added; null when none carries price and rate. */
21 commission: number | null;
22 };
23
24 /** Deals from the year of `on` (in the order given), and earlier years newest first, each in the order given. */
25 export function byYear<T extends Settled>(deals: readonly T[], on: string): { current: T[]; earlier: Year<T>[] } {
26 const now = on.slice(0, 4);
27 const current: T[] = [];
28 const years = new Map<string, Year<T>>();
29 for (const deal of deals) {
30 const year = deal.day.slice(0, 4);
31 if (year >= now) {
32 current.push(deal);
33 continue;
34 }
35 let y = years.get(year);
36 if (y === undefined) years.set(year, (y = { year, deals: [], closed: 0, fell: 0, volume: null, commission: null }));
37 y.deals.push(deal);
38 if (deal.fell) {
39 y.fell++;
40 continue;
41 }
42 y.closed++;
43 if (deal.sale_price !== null) {
44 y.volume = (y.volume ?? 0) + deal.sale_price;
45 if (deal.commission_rate !== null) y.commission = (y.commission ?? 0) + commission(deal.sale_price, deal.commission_rate);
46 }
47 }
48 return { current, earlier: [...years.values()].sort((a, b) => b.year.localeCompare(a.year)) };
49 }
50
51 /** "2025 · 14 closed · $5,120,000 · commission $128,000 · 2 fell through". */
52 export function yearLine(y: Year<unknown>): string {
53 const parts = [y.year];
54 if (y.closed > 0 || y.fell === 0) parts.push(`${y.closed} closed`);
55 if (y.volume !== null) parts.push(dollars(y.volume));
56 if (y.commission !== null) parts.push(`commission ${dollars(y.commission)}`);
57 if (y.fell > 0) parts.push(`${y.fell} fell through`);
58 return parts.join(" · ");
59 }
static/app.css +11
@@ -1121,6 +1121,17 @@ a.marker {
11211121 }
11221122 }
11231123
1124 .closed-year {
1125 margin-top: var(--space-2);
1126 }
1127 .closed-year summary {
1128 cursor: pointer;
1129 min-height: 2.75rem;
1130 display: flex;
1131 align-items: center;
1132 font-weight: 600;
1133 }
1134
11241135 /* The broker file: plain figures and lists on the screen; on paper, black on white with no tools. */
11251136 .file-facts {
11261137 display: grid;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs