${escapeHtml(row.title)}
Due ${formatDate(row.due_date)}
const express = require('express');
const { Pool } = require('pg');
const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const port = Number(process.env.PORT || 3000);
const tags = new Set(['permits', 'kitchen', 'marketing']);
const statuses = new Set(['todo', 'doing', 'done']);
const seedTasks = [
['county-permit', 'Submit county food permit', 'permits', '2026-08-04', 'doing'],
['fire-review', 'Book fire safety review', 'permits', '2026-08-08', 'todo'],
['vendor-license', 'Confirm street vendor license', 'permits', '2026-08-11', 'todo'],
['taco-test', 'Finish smoked mushroom taco test', 'kitchen', '2026-07-23', 'done'],
['cooler', 'Buy the service line cooler', 'kitchen', '2026-07-27', 'doing'],
['allergen-card', 'Print the allergen reference card', 'kitchen', '2026-07-29', 'todo'],
['supplier-call', 'Confirm first produce delivery', 'kitchen', '2026-07-31', 'todo'],
['menu-board', 'Approve the opening menu board', 'marketing', '2026-07-25', 'done'],
['photo-day', 'Shoot menu photos at golden hour', 'marketing', '2026-08-02', 'todo'],
['map-listing', 'Publish the weekly stop map', 'marketing', '2026-08-06', 'todo'],
['opening-flyer', 'Send the opening day flyer', 'marketing', '2026-08-12', 'todo'],
['friends-preview', 'Invite neighbors to the preview night', 'marketing', '2026-08-14', 'todo'],
];
app.use(express.json({ limit: '32kb' }));
app.use(express.urlencoded({ extended: false }));
const cleanTask = (input, partial = false) => {
const task = {};
if (!partial || input.title !== undefined) {
task.title = String(input.title || '').trim();
if (!task.title || task.title.length > 120) throw new Error('Title must be between 1 and 120 characters.');
}
if (!partial || input.tag !== undefined) {
task.tag = String(input.tag || '');
if (!tags.has(task.tag)) throw new Error('Tag must be permits, kitchen, or marketing.');
}
if (!partial || input.dueDate !== undefined) {
task.dueDate = String(input.dueDate || '');
if (!/^\d{4}-\d{2}-\d{2}$/.test(task.dueDate)) throw new Error('Due date must use YYYY-MM-DD.');
}
if (!partial || input.status !== undefined) {
task.status = String(input.status || 'todo');
if (!statuses.has(task.status)) throw new Error('Status must be todo, doing, or done.');
}
return task;
};
const escapeHtml = (value) => String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"');
const formatDate = (value) => value instanceof Date
? value.toISOString().slice(0, 10)
: String(value).slice(0, 10);
const toTask = (row) => ({
id: String(row.id),
title: row.title,
tag: row.tag,
dueDate: formatDate(row.due_date),
status: row.status,
});
async function seed() {
for (const [seedKey, title, tag, dueDate, status] of seedTasks) {
await pool.query(
`insert into launch_tasks (seed_key, title, tag, due_date, status)
values ($1, $2, $3, $4, $5)
on conflict (seed_key) do nothing`,
[seedKey, title, tag, dueDate, status],
);
}
}
app.get('/health', async (_request, response) => {
await pool.query('select 1');
response.json({ ok: true });
});
app.get('/api/tasks', async (request, response) => {
const page = Math.max(1, Number.parseInt(request.query.page || '1', 10) || 1);
const pageSize = Math.min(50, Math.max(1, Number.parseInt(request.query.pageSize || '10', 10) || 10));
const values = [];
const conditions = [];
if (request.query.status) {
if (!statuses.has(request.query.status)) return response.status(400).json({ error: 'Unknown status.' });
values.push(request.query.status);
conditions.push(`status = $${values.length}`);
}
if (request.query.tag) {
if (!tags.has(request.query.tag)) return response.status(400).json({ error: 'Unknown tag.' });
values.push(request.query.tag);
conditions.push(`tag = $${values.length}`);
}
if (request.query.search) {
values.push(`%${String(request.query.search).slice(0, 80)}%`);
conditions.push(`title ilike $${values.length}`);
}
const where = conditions.length ? `where ${conditions.join(' and ')}` : '';
const count = await pool.query(`select count(*)::int as total from launch_tasks ${where}`, values);
values.push(pageSize, (page - 1) * pageSize);
const result = await pool.query(
`select id, title, tag, due_date, status
from launch_tasks ${where}
order by due_date, id
limit $${values.length - 1} offset $${values.length}`,
values,
);
const total = count.rows[0].total;
response.json({
tasks: result.rows.map(toTask),
page: { number: page, size: pageSize, total, pages: Math.max(1, Math.ceil(total / pageSize)) },
});
});
app.post('/api/tasks', async (request, response) => {
try {
const task = cleanTask(request.body);
const result = await pool.query(
`insert into launch_tasks (title, tag, due_date, status)
values ($1, $2, $3, $4)
returning id, title, tag, due_date, status`,
[task.title, task.tag, task.dueDate, task.status],
);
response.status(201).json(toTask(result.rows[0]));
} catch (error) {
response.status(400).json({ error: error.message });
}
});
app.patch('/api/tasks/:id', async (request, response) => {
try {
const updates = cleanTask(request.body, true);
const entries = Object.entries(updates);
if (!entries.length) return response.status(400).json({ error: 'Send at least one task field.' });
const columns = { title: 'title', tag: 'tag', dueDate: 'due_date', status: 'status' };
const values = entries.map(([, value]) => value);
const set = entries.map(([key], index) => `${columns[key]} = $${index + 1}`);
values.push(request.params.id);
const result = await pool.query(
`update launch_tasks set ${set.join(', ')}, updated_at = now()
where id = $${values.length}
returning id, title, tag, due_date, status`,
values,
);
if (!result.rowCount) return response.status(404).json({ error: 'Task not found.' });
response.json(toTask(result.rows[0]));
} catch (error) {
response.status(400).json({ error: error.message });
}
});
app.delete('/api/tasks/:id', async (request, response) => {
const result = await pool.query('delete from launch_tasks where id = $1', [request.params.id]);
if (!result.rowCount) return response.status(404).json({ error: 'Task not found.' });
response.status(204).end();
});
app.get('/', async (_request, response) => {
const result = await pool.query(
`select id, title, tag, due_date, status from launch_tasks
order by case status when 'doing' then 0 when 'todo' then 1 else 2 end, due_date, id`,
);
const cards = result.rows.map((row) => `
Due ${formatDate(row.due_date)}${escapeHtml(row.title)}
${result.rowCount} moving pieces, one clear board. Get the permits, kitchen, and neighborhood ready together.