worldwide todo list: public todo list of 68 real, sourced problems (planet to street) at /projects/worldwide-todo. worldtodo schema (categories, tasks, change_requests), idempotent seed on deploy, public add/edit/delete requests with Turnstile + salted daily-rotating IP-hash rate limit, owner-only review queue (approve, edit & approve, reject) with live updates over /ws/worldtodo(-owner), JSON API, detail pages with OSM map and history; listed in projects, header, /start and sitemap
Some checks failed
Deploy production / deploy (push) Failing after 9s

This commit is contained in:
2026-10-26 01:25:59 +00:00
parent 0ee981a96b
commit 5e812271e1
58 changed files with 47689 additions and 9 deletions

87
scripts/seed-worldtodo.ts Normal file
View File

@@ -0,0 +1,87 @@
import { readFile } from 'node:fs/promises';
import postgres from 'postgres';
// Seeds the Worldwide Todo List (schema worldtodo) from data/worldtodo-seed.json, built by
// scripts/build-worldtodo-seed.py (68 sourced tasks as of 2026-10-26). Idempotent: categories
// are upserted, tasks are inserted with ON CONFLICT (slug) DO NOTHING, so owner edits and
// approved deletes (soft, the row stays) are never overwritten. Runs on every deploy
// (scripts/deploy-remote.sh) as the schema owner; self-contained so it works in the app image.
// bun scripts/seed-worldtodo.ts
if (!process.env.DB_URL) throw new Error('DB_URL is required');
type SeedTask = {
slug: string;
title: string;
description: string;
scale: string;
category: string;
location_name: string;
country_code: string | null;
lat: number | null;
lon: number | null;
status: string;
source_urls: string[];
source_date: string | null;
as_of: string;
confidence: string;
verification_note?: string | null;
};
// Lucide icon key (#lib/worldtodo/icons) and an accent colour for the icon tile.
const CATEGORIES: Record<string, { label: string; icon: string; colour: string }> = {
climate: { label: 'Climate', icon: 'thermometer', colour: '#ef4444' },
environment: { label: 'Environment', icon: 'leaf', colour: '#22c55e' },
energy: { label: 'Energy', icon: 'zap', colour: '#eab308' },
food: { label: 'Food', icon: 'wheat', colour: '#d97706' },
governance: { label: 'Governance', icon: 'landmark', colour: '#6366f1' },
health: { label: 'Health', icon: 'heart-pulse', colour: '#ec4899' },
housing: { label: 'Housing', icon: 'house', colour: '#f97316' },
humanitarian: { label: 'Humanitarian', icon: 'hand-heart', colour: '#e11d48' },
infrastructure: { label: 'Infrastructure', icon: 'construction', colour: '#0ea5e9' },
mobility: { label: 'Mobility', icon: 'bike', colour: '#14b8a6' },
safety: { label: 'Safety', icon: 'shield-alert', colour: '#f59e0b' },
waste: { label: 'Waste', icon: 'recycle', colour: '#84cc16' },
water: { label: 'Water', icon: 'droplet', colour: '#3b82f6' }
};
const seed = JSON.parse(
await readFile(new URL('../data/worldtodo-seed.json', import.meta.url), 'utf8')
) as { categories: string[]; tasks: SeedTask[] };
const client = postgres(process.env.DB_URL, { max: 1, onnotice: () => {} });
try {
await client.begin(async (tx) => {
for (const slug of seed.categories) {
const c = CATEGORIES[slug] ?? { label: slug, icon: 'list-todo', colour: '#a3a3a3' };
await tx`insert into worldtodo.categories (slug, label, icon, colour)
values (${slug}, ${c.label}, ${c.icon}, ${c.colour})
on conflict (slug) do update set label = excluded.label, icon = excluded.icon,
colour = excluded.colour`;
}
let inserted = 0;
for (const t of seed.tasks) {
const rows = await tx`
insert into worldtodo.tasks (slug, title, description, scale, category, location_name,
country_code, lat, lon, status, source_urls, source_date, as_of, confidence,
verification_note)
values (${t.slug}, ${t.title}, ${t.description}, ${t.scale}::worldtodo.scale,
${t.category}, ${t.location_name}, ${t.country_code ?? null}, ${t.lat ?? null},
${t.lon ?? null}, ${t.status}::worldtodo.status, ${t.source_urls},
${t.source_date ?? null}, ${t.as_of}, ${t.confidence ?? 'high'},
${t.verification_note ?? null})
on conflict (slug) do nothing
returning id`;
inserted += rows.length;
}
const [{ n }] = await tx`select count(*)::int as n from worldtodo.tasks
where slug = any(${seed.tasks.map((t) => t.slug)})`;
if (n !== seed.tasks.length)
throw new Error(`Expected ${seed.tasks.length} seed tasks, found ${n}`);
console.log(
`Worldwide todo: ${seed.categories.length} categories, ${inserted} new of ${seed.tasks.length} seed tasks (all present)`
);
});
} finally {
await client.end();
}