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

View File

@@ -30,4 +30,5 @@ export * from './stats';
export * from './pubg-map';
export * from './ai';
export * from './movies';
export * from './worldtodo';
export * from './osu-goals';

View File

@@ -27,3 +27,4 @@ export const dataholdersSchema = pgSchema('dataholders');
export const statsSchema = pgSchema('stats');
export const aiSchema = pgSchema('ai');
export const moviesSchema = pgSchema('movies');
export const worldtodoSchema = pgSchema('worldtodo');

View File

@@ -0,0 +1,124 @@
import { sql } from 'drizzle-orm';
import {
bigint,
check,
customType,
date,
doublePrecision,
index,
jsonb,
text,
uniqueIndex
} from 'drizzle-orm/pg-core';
import { worldtodoSchema } from './schemas';
import { timestamptz } from './helpers';
// The Worldwide Todo List (/projects/worldwide-todo): a public list of real problems phrased
// as todos. Anyone can *request* an add, edit or delete (change_requests); nothing changes
// until the owner approves it in the queue. No submitter data is stored beyond an optional
// display name: no IP, email or user agent (rate limiting is in memory, see
// #lib/server/worldtodo/limit). Seeded by scripts/seed-worldtodo.ts from data/worldtodo-seed.json.
// Kept in sync with #lib/worldtodo/shared (a unit test checks it); the schema folder is copied
// into the app image on its own for the scripts, so it must not import from outside it.
const WORLDTODO_SCALES = ['planet', 'region', 'country', 'city', 'street'] as const;
const WORLDTODO_STATUSES = ['open', 'in_progress', 'done'] as const;
const WORLDTODO_REQUEST_KINDS = ['add', 'edit', 'delete'] as const;
const WORLDTODO_REQUEST_STATUSES = ['pending', 'approved', 'rejected'] as const;
const tsvector = customType<{ data: string }>({ dataType: () => 'tsvector' });
export const worldtodoScale = worldtodoSchema.enum('scale', WORLDTODO_SCALES);
export const worldtodoStatus = worldtodoSchema.enum('status', WORLDTODO_STATUSES);
export const worldtodoRequestKind = worldtodoSchema.enum('req_kind', WORLDTODO_REQUEST_KINDS);
export const worldtodoRequestStatus = worldtodoSchema.enum(
'req_status',
WORLDTODO_REQUEST_STATUSES
);
export const worldtodoCategories = worldtodoSchema.table('categories', {
slug: text('slug').primaryKey(),
label: text('label').notNull(),
/** Icon key in #lib/worldtodo/icons (a lucide icon). */
icon: text('icon').notNull(),
/** Hex accent for the icon tile only. */
colour: text('colour').notNull()
});
export const worldtodoTasks = worldtodoSchema.table(
'tasks',
{
id: bigint('id', { mode: 'number' }).primaryKey().generatedByDefaultAsIdentity(),
slug: text('slug').notNull(),
title: text('title').notNull(),
description: text('description').notNull(),
scale: worldtodoScale('scale').notNull(),
category: text('category')
.notNull()
.references(() => worldtodoCategories.slug),
locationName: text('location_name').notNull(),
/** ISO 3166-1 alpha-2; null for planet-scale or multinational tasks. */
countryCode: text('country_code'),
lat: doublePrecision('lat'),
lon: doublePrecision('lon'),
status: worldtodoStatus('status').notNull().default('open'),
sourceUrls: text('source_urls').array().notNull(),
/** Date of the main source as given ('2026-09', '2026-10-21'). */
sourceDate: text('source_date'),
confidence: text('confidence').notNull().default('high'),
/** Owner-facing caveat, also shown subtly on the detail page. */
verificationNote: text('verification_note'),
/** When the facts were last checked. */
asOf: date('as_of').notNull(),
createdAt: timestamptz('created_at').notNull().defaultNow(),
updatedAt: timestamptz('updated_at').notNull().defaultNow(),
/** Soft delete (approved delete requests); hard purge is a manual owner action. */
deletedAt: timestamptz('deleted_at'),
search: tsvector('search').generatedAlwaysAs(
sql`to_tsvector('simple', title || ' ' || description || ' ' || location_name)`
)
},
(t) => [
uniqueIndex('tasks_slug_key').on(t.slug),
index('tasks_search_idx').using('gin', t.search),
index('tasks_scale_status_idx')
.on(t.scale, t.status)
.where(sql`deleted_at IS NULL`),
check('tasks_title_check', sql`char_length(${t.title}) BETWEEN 5 AND 140`),
check('tasks_description_check', sql`char_length(${t.description}) <= 2000`),
check('tasks_lat_check', sql`${t.lat} BETWEEN -90 AND 90`),
check('tasks_lon_check', sql`${t.lon} BETWEEN -180 AND 180`),
check('tasks_sources_check', sql`cardinality(${t.sourceUrls}) >= 1`),
check('tasks_confidence_check', sql`${t.confidence} IN ('high','medium')`)
]
);
export const worldtodoChangeRequests = worldtodoSchema.table(
'change_requests',
{
id: bigint('id', { mode: 'number' }).primaryKey().generatedByDefaultAsIdentity(),
kind: worldtodoRequestKind('kind').notNull(),
/** Null for 'add'. */
taskId: bigint('task_id', { mode: 'number' }).references(() => worldtodoTasks.id),
/** add: full task fields; edit: only changed fields; delete: {}. Whitelisted keys only. */
payload: jsonb('payload').$type<Record<string, unknown>>().notNull().default({}),
/** What the owner actually applied (differs from payload after "edit & approve"). */
applied: jsonb('applied').$type<Record<string, unknown>>(),
/** The task's updated_at when the request was filed, to flag stale requests. */
taskUpdatedAt: timestamptz('task_updated_at'),
reason: text('reason').notNull(),
/** Optional, the ONLY submitter info stored; shown nowhere except the owner queue. */
displayName: text('display_name'),
status: worldtodoRequestStatus('status').notNull().default('pending'),
ownerNote: text('owner_note'),
createdAt: timestamptz('created_at').notNull().defaultNow(),
decidedAt: timestamptz('decided_at')
},
(t) => [
index('change_requests_status_idx').on(t.status, t.createdAt),
index('change_requests_task_idx').on(t.taskId, t.status),
check('change_requests_reason_check', sql`char_length(${t.reason}) BETWEEN 10 AND 1000`),
check('change_requests_display_name_check', sql`char_length(${t.displayName}) <= 40`),
check('change_requests_task_check', sql`((${t.kind} = 'add') = (${t.taskId} IS NULL))`)
]
);