house tracker: contact status per listing (whose turn, last message, viewing) and labelled filters
Some checks failed
Deploy production / deploy (push) Has been cancelled

This commit is contained in:
2026-10-25 13:53:01 +00:00
parent df081b8058
commit 7d6cee26a8
8 changed files with 26860 additions and 94 deletions

View File

@@ -4,7 +4,9 @@
// bun run house add --url <link> --city Gouda --price 1422 --area 84 --bedrooms 2
// bun run house add --json listing.json (or `--json -` for stdin; may be an array)
// bun run house update <id|url> --status viewing --notes "Sat 14:00"
// bun run house list [--all] [--city gouda] [--offer buy] [--json]
// bun run house update <id|url> --contact waiting_on_them --contact-at 2026-10-06 --contact-by me \
// --contact-note "Applied via form" --contact-ref "gmail thread …"
// bun run house list [--all] [--city gouda] [--offer buy] [--contact any|<s>] [--json]
// bun run house remove <id|url> [--hard] (soft remove = marked gone)
// bun run house restore <id|url>
//
@@ -37,7 +39,8 @@ const usage = `usage: bun run house <add|update|list|remove|restore> [options]
add [--json <file|->] [field flags] insert, or update when the same link/address exists
update <id|url> [field flags] change only the given fields (use "" to clear one)
list [--all] [--city <slug>] [--offer rent|buy] [--status <s>] [--limit n] [--json]
list [--all] [--city <slug>] [--offer rent|buy] [--status <s>] [--contact any|<s>]
[--limit n] [--json]
remove <id|url> [--hard] mark gone (or delete with --hard)
restore <id|url> undo a soft remove
@@ -47,6 +50,9 @@ field flags: --url --offer rent|buy --type apartment|house|room|studio|other --t
--contract-type --agent --image-url --description --notes --status
interested|contacted|viewing|applied|rejected --lat --lng --offered-at
--first-seen (when it was shared) --source --source-id
contact: --contact waiting_on_me|not_contacted|waiting_on_them|viewing|closed
--contact-at <date> (last message) --contact-by me|them --contact-note (one line)
--contact-ref (mail thread / chat) --viewing-at <date time>
other: --dry-run (print, don't write)`;
const fieldFlags = [
@@ -80,7 +86,13 @@ const fieldFlags = [
'first-seen',
'source',
'source-id',
'external-id'
'external-id',
'contact',
'contact-at',
'contact-by',
'contact-note',
'contact-ref',
'viewing-at'
] as const;
const { values: flags, positionals } = parseArgs({
@@ -145,7 +157,8 @@ const sql = postgres(dbUrl, { max: 1, onnotice: () => {} });
type Tx = postgres.TransactionSql;
const summary = sql`id, source, source_id, offer, property_type, title, city, city_slug,
price_eur, area_m2, rooms, bedrooms, status, removed_at, url`;
price_eur, area_m2, rooms, bedrooms, status, removed_at, url, contact_status, contact_at,
contact_by, contact_note, contact_ref, viewing_at`;
async function findId(ref: string | undefined): Promise<number> {
if (!ref) throw new Error('give a listing id or url');
@@ -336,12 +349,20 @@ async function list() {
const city = flag('city');
const offer = flag('offer');
const status = flag('status');
const contact = flag('contact');
const rows = await sql`select ${summary}, first_seen_at, notes from house.listings
where true
${flag('all') ? sql`` : sql`and removed_at is null`}
${typeof city === 'string' ? sql`and city_slug = ${city}` : sql``}
${typeof offer === 'string' ? sql`and offer = ${offer}` : sql``}
${typeof status === 'string' ? sql`and status = ${status}` : sql``}
${
contact === 'any'
? sql`and contact_status is not null`
: typeof contact === 'string'
? sql`and contact_status = ${contact}`
: sql``
}
order by first_seen_at desc limit ${limit}`;
if (flag('json')) console.log(JSON.stringify(rows, null, 2));
else print(rows);
@@ -378,6 +399,17 @@ function print(rows: readonly Record<string, unknown>[]) {
console.log(
`${r.action ? `${r.action} ` : ''}#${r.id} [${r.source}] ${r.title ?? '(no title)'}, ${r.city ?? '?'} · ${bits.join(' · ')}\n ${r.url}`
);
if (r.contact_status) {
const day = (d: unknown) => (d instanceof Date ? d.toISOString().slice(0, 16) : d);
const contact = [
r.contact_status,
r.viewing_at && `viewing ${day(r.viewing_at)}`,
r.contact_at && `last ${day(r.contact_at)}${r.contact_by ? ` by ${r.contact_by}` : ''}`,
r.contact_note,
r.contact_ref && `ref: ${r.contact_ref}`
].filter(Boolean);
console.log(` contact: ${contact.join(' · ')}`);
}
}
}