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
Some checks failed
Deploy production / deploy (push) Failing after 9s
This commit is contained in:
87
scripts/seed-worldtodo.ts
Normal file
87
scripts/seed-worldtodo.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user