Public: anyone can read this; the crew can change it.
Open the meeting on a finished launch: the 412 Alder Lane sample shows all six assets approved by its sample agent #54
Check · Review · Land · Live done — Landed and live in production.
mason/listing-launch:mason/52-open-the-meeting-on-a-finished-launch-th → main 1 commit fork
6 files
changed +80
−8
README.md +4 −1
| @@ -9,7 +9,10 @@ to make a listing of your own. | ||
| 9 | 9 | |
| 10 | 10 | 1. **Open the sample.** On the first screen, under "Launched", press the |
| 11 | 11 | card for *412 Alder Lane, Portland*. Its page shows the price, the |
| 12 | photos and the words, in tabs: MLS, Social, Flyer and Email. | |
| 12 | photos and the words, in tabs: MLS, Social, Flyer and Email. It opens | |
| 13 | finished: all six assets approved (the pill reads "Ready to send"), each signed off by "Morgan Reyes | |
| 14 | (sample)", so you see what a launch looks like at the end. (Birchwood | |
| 15 | Court stays at 0, and your own copy starts at 0.) | |
| 13 | 16 | 2. **Read an asset and its check.** The MLS tab has the description in a box, |
| 14 | 17 | and beside it the **Fair Housing** check: green when it is clear. Open the |
| 15 | 18 | card for *27 Birchwood Court, Boise* to see it catch something: the |
src/db.ts +7
| @@ -97,6 +97,13 @@ export const migrations: string[] = [ | ||
| 97 | 97 | `ALTER TABLE listing ADD COLUMN sold_price INTEGER`, |
| 98 | 98 | // The price before it was lowered, while it stands below it; the price improvement assets are written from it. |
| 99 | 99 | `ALTER TABLE listing ADD COLUMN was_price INTEGER`, |
| 100 | // Alder Lane, the planted sample (author 'sample', its own headshot), opens finished: each untouched | |
| 101 | // asset is signed off by its sample agent, through 'sample', never a person's door. An edited, | |
| 102 | // already-approved or removed sample is left alone; the one earlier approval planted is re-signed. | |
| 103 | `UPDATE asset SET approved_by_id = 'sample', approved_by_name = 'Morgan Reyes (sample)', | |
| 104 | approved_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), approved_via = 'sample' | |
| 105 | WHERE edited = 0 AND (approved_at IS NULL OR approved_by_id = 'sample') | |
| 106 | AND listing_id IN (SELECT id FROM listing WHERE author_id = 'sample' AND headshot = 'sample-agent.svg')`, | |
| 100 | 107 | ]; |
| 101 | 108 | |
| 102 | 109 | export function openDb(dir = dataDir): Database { |
src/pages/listing.ts +2 −2
| @@ -118,9 +118,9 @@ export const draftBand = (l: Launched, kind: "flyer" | "email"): string => | ||
| 118 | 118 | |
| 119 | 119 | const DOORS: Record<string, string> = { page: "the page", api: "the API", tty: "the terminal" }; |
| 120 | 120 | |
| 121 | /** Where an approval came from, beside who gave it; nothing for one given before it was recorded. */ | |
| 121 | /** Where an approval came from, beside who gave it; nothing for one given before it was recorded, or by a sample (its name already says so). */ | |
| 122 | 122 | const via = (door: string | null): string => |
| 123 | door !== null && door in DOORS ? ` · via ${DOORS[door]}` : ""; | |
| 123 | door !== null && door !== "sample" && door in DOORS ? ` · via ${DOORS[door]}` : ""; | |
| 124 | 124 | |
| 125 | 125 | /** The approve control beside Copy: a button naming the flags, or who signed off and a way to take it back. */ |
| 126 | 126 | export function approval(l: Launched, a: Asset): string { |
src/sample.test.ts +59 −2
| @@ -4,9 +4,12 @@ import { expect, test } from "bun:test"; | ||
| 4 | 4 | import { mkdtempSync } from "node:fs"; |
| 5 | 5 | import { tmpdir } from "node:os"; |
| 6 | 6 | import { join } from "node:path"; |
| 7 | import { openDb } from "./db.ts"; | |
| 7 | import { getListing, launchListing } from "./actions.ts"; | |
| 8 | import { migrate, openDb } from "./db.ts"; | |
| 9 | import { kit } from "./kit.ts"; | |
| 10 | import { draftBand } from "./pages/listing.ts"; | |
| 8 | 11 | import { check } from "./fairhousing.ts"; |
| 9 | import { seed } from "./sample.ts"; | |
| 12 | import { SAMPLE, seed } from "./sample.ts"; | |
| 10 | 13 | |
| 11 | 14 | const fresh = () => openDb(mkdtempSync(join(tmpdir(), "sample-"))); |
| 12 | 15 | const count = (db: ReturnType<typeof fresh>) => |
| @@ -45,3 +48,57 @@ test("the house opens amber, the condo and Alder Lane green", () => { | ||
| 45 | 48 | expect(flags("88 Meridian%")).toBe(0); |
| 46 | 49 | expect(flags("412 Alder%")).toBe(0); |
| 47 | 50 | }); |
| 51 | ||
| 52 | const ANON = { role: "anon" } as const; | |
| 53 | const idOf = (db: ReturnType<typeof fresh>, like: string) => | |
| 54 | db.query<{ id: number }, [string]>("SELECT id FROM listing WHERE address LIKE ?1").get(like)!.id; | |
| 55 | const approvedOf = (db: ReturnType<typeof fresh>, like: string) => { | |
| 56 | const r = getListing(db, ANON, { id: idOf(db, like) }); | |
| 57 | if (!r.ok) throw new Error("no listing"); | |
| 58 | return r.value.assets.filter((a) => a.approval !== null); | |
| 59 | }; | |
| 60 | ||
| 61 | test("Alder Lane opens with all six assets approved by its sample agent; Birchwood has none", () => { | |
| 62 | const db = fresh(); | |
| 63 | seed(db); | |
| 64 | const alder = approvedOf(db, "412 Alder%"); | |
| 65 | expect(alder).toHaveLength(6); | |
| 66 | for (const a of alder) expect(a.approval).toMatchObject({ by_name: "Morgan Reyes (sample)", via: "sample" }); | |
| 67 | expect(approvedOf(db, "27 Birchwood%")).toHaveLength(0); | |
| 68 | expect(approvedOf(db, "88 Meridian%")).toHaveLength(0); | |
| 69 | const listing = getListing(db, ANON, { id: idOf(db, "412 Alder%") }); | |
| 70 | expect(listing.ok && kit(listing.value)).toContain("Approved by Morgan Reyes (sample) on "); | |
| 71 | expect(listing.ok && draftBand(listing.value, "flyer")).toBe(""); | |
| 72 | expect(listing.ok && draftBand(listing.value, "email")).toBe(""); | |
| 73 | }); | |
| 74 | ||
| 75 | test("the migration approves an old sample once, and leaves edited or approved rows alone", () => { | |
| 76 | const db = fresh(); | |
| 77 | seed(db); | |
| 78 | const id = idOf(db, "412 Alder%"); | |
| 79 | db.run("UPDATE asset SET approved_by_id = NULL, approved_by_name = NULL, approved_at = NULL, approved_via = NULL"); | |
| 80 | db.run(`UPDATE asset SET edited = 1 WHERE kind = 'flyer' AND listing_id = ${id}`); | |
| 81 | db.run(`UPDATE asset SET approved_by_id = 'u1', approved_by_name = 'Pat', approved_at = '2026-01-01T00:00:00Z', approved_via = 'page' WHERE kind = 'email' AND listing_id = ${id}`); | |
| 82 | db.run("DELETE FROM schema_version WHERE version = (SELECT max(version) FROM schema_version)"); | |
| 83 | migrate(db); | |
| 84 | const rows = () => | |
| 85 | db.query<{ kind: string; approved_by_name: string | null }, [number]>("SELECT kind, approved_by_name FROM asset WHERE listing_id = ?1").all(id); | |
| 86 | const names = Object.fromEntries(rows().map((r) => [r.kind, r.approved_by_name])); | |
| 87 | expect(names.mls).toBe("Morgan Reyes (sample)"); | |
| 88 | expect(names.flyer).toBeNull(); | |
| 89 | expect(names.email).toBe("Pat"); | |
| 90 | expect(approvedOf(db, "27 Birchwood%")).toHaveLength(0); | |
| 91 | // Second time: nothing to run, so a later withdrawal stays withdrawn. | |
| 92 | db.run("UPDATE asset SET approved_by_id = NULL, approved_by_name = NULL, approved_at = NULL, approved_via = NULL WHERE kind = 'mls'"); | |
| 93 | migrate(db); | |
| 94 | expect(rows().find((r) => r.kind === "mls")!.approved_by_name).toBeNull(); | |
| 95 | }); | |
| 96 | ||
| 97 | test("a copy started from the sample starts at 0 approved", () => { | |
| 98 | const db = fresh(); | |
| 99 | seed(db); | |
| 100 | const me = { role: "user", id: "u1", name: "me" } as const; | |
| 101 | const made = launchListing(db, me, { fields: { ...SAMPLE, address: "9 Copy Way, Portland, OR 97214", features: SAMPLE.features.join("\n") } }); | |
| 102 | expect(made.ok).toBe(true); | |
| 103 | expect(made.ok && made.value.assets.filter((a) => a.approval !== null)).toHaveLength(0); | |
| 104 | }); |
src/sample.ts +3 −3
| @@ -136,11 +136,11 @@ export function seed(db: Database): void { | ||
| 136 | 136 | const done = db.query("INSERT OR IGNORE INTO seeded (name) VALUES (?1)").run(s.name); |
| 137 | 137 | if (done.changes === 0) continue; |
| 138 | 138 | const made = insertListing(db, { id: "sample", name: "Sample listing" }, s.listing, s.photos, s.headshot); |
| 139 | // The first sample's MLS text is approved, so the preview's property page shows words, not a 404. | |
| 139 | // Alder Lane opens finished: every asset signed off by its sample agent, never by a person's door. | |
| 140 | 140 | if (s.name === "sample") |
| 141 | 141 | db.query( |
| 142 | "UPDATE asset SET approved_by_id = 'sample', approved_by_name = 'Sample listing', approved_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), approved_via = 'page' WHERE listing_id = ?1 AND kind = 'mls'", | |
| 143 | ).run(made.id); | |
| 142 | "UPDATE asset SET approved_by_id = 'sample', approved_by_name = ?2, approved_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), approved_via = 'sample' WHERE listing_id = ?1", | |
| 143 | ).run(made.id, `${s.listing.agent_name} (sample)`); | |
| 144 | 144 | } |
| 145 | 145 | // A sample planted before statuses existed is marked sold once too; after that it is the owner's to change. |
| 146 | 146 | for (const s of SAMPLES) { |
src/walkthrough.test.ts +5
| @@ -87,6 +87,11 @@ test("every step of the section works, with the labels it names", async () => { | ||
| 87 | 87 | expect(sampleHtml).toMatch(new RegExp(`role="tab"[^>]*>${tab}[ <]`)); |
| 88 | 88 | } |
| 89 | 89 | |
| 90 | // The sample opens finished, signed off by its sample agent; the Boise one is not. | |
| 91 | expect(homeHtml).toContain("Ready to send"); | |
| 92 | expect(sampleHtml).toContain("Approved by Morgan Reyes (sample)"); | |
| 93 | expect(sampleHtml).not.toContain("Draft — not approved"); | |
| 94 | ||
| 90 | 95 | // 2. Read an asset and its check; the Boise sample is the one that catches something. |
| 91 | 96 | expect(sampleHtml).toContain("Fair Housing: clear"); |
| 92 | 97 | expect(section).toContain("27 Birchwood Court"); |