phase: 04-channel-crud-api
plan: 01
subsystem: api
tags: [channels, crud, slug, migration, scoped-reset, ingest-guard]
dependency_graph:
requires: [03-02]
provides: [POST /channels, GET /channels, scoped POST /admin/reset, channel-guarded POST /ingest]
affects: [api.py, schema.sql]
tech_stack:
added: []
patterns: [slug-as-id derivation, UniqueViolation-before-Exception ordering, HTTPException re-raise guard]
key_files:
created: [migrate_category.sql]
modified: [schema.sql, api.py, smoke_test.sh]
decisions:
- "slug derived server-side via name.lower().replace(' ', '-') β€” client never provides id"
- "UniqueViolation caught before generic Exception in create_channel β€” 409 not 500"
- "re.fullmatch (not re.match) for slug validation β€” anchors both start and end"
- "except HTTPException: raise guard in ingest before except Exception β€” 404 not swallowed as 500"
- "Global reset preserves 'general' channel via DELETE FROM channels WHERE id != 'general'"
metrics:
duration: "1m 52s"
completed: "2026-03-24T19:53:15Z"
tasks_completed: 3
tasks_total: 3
files_changed: 4


Phase 04 Plan 01: Channel CRUD API Summary

One-liner: Channel CRUD API with server-derived slug IDs, 409/422 error guards, scoped reset, and ingest channel-existence validation.

What Was Built

Added two new routes and extended two existing routes in api.py to support channel management:

  • POST /channels β€” creates a channel from name + optional category, derives id via slug (lowercase + spaces-to-hyphens), returns 201 with full channel object. Returns 409 on duplicate name/slug, 422 on names that produce invalid slugs (umlauts, special chars).
  • GET /channels β€” returns all channels sorted alphabetically by name with id, name, category fields.
  • POST /admin/reset β€” extended with optional channel_id query parameter. With param: scoped DELETE WHERE channel_id. Without param: global delete of all chunks + all non-general channels.
  • POST /ingest β€” now validates channel existence before calling ingest_file(). Returns 404 if channel not found.

Schema updated with category TEXT column on channels table. Idempotent migration file created for existing DBs.

Tasks Completed

Task Name Commit Files
1 Schema migration + Pydantic models 385b708 schema.sql, migrate_category.sql, api.py
2 POST /channels and GET /channels a9860c8 api.py
3 Scoped reset, ingest guard, smoke tests a7339ab api.py, smoke_test.sh

Decisions Made

  1. Slug derivation server-side only β€” client posts name, server computes id. Eliminates client/server id mismatch.
  2. UniqueViolation before Exception β€” except psycopg2.errors.UniqueViolation must appear before except Exception in create_channel. Duplicate names produce 409, not 500.
  3. re.fullmatch not re.match β€” re.match only anchors at start; re.fullmatch requires the entire slug to match, preventing trailing invalid chars from slipping through.
  4. except HTTPException: raise guard in ingest β€” without this, the 404 for missing channel would be caught by except Exception and re-raised as 500.
  5. Global reset preserves 'general' β€” DELETE FROM channels WHERE id != 'general' ensures the seed channel always exists after reset.

Deviations from Plan

None β€” plan executed exactly as written.

Known Stubs

None β€” all routes are fully wired to DB and return live data.

Self-Check: PASSED

  • FOUND: schema.sql
  • FOUND: migrate_category.sql
  • FOUND: api.py
  • FOUND: smoke_test.sh
  • FOUND: 04-01-SUMMARY.md
  • FOUND commit 385b708 (Task 1)
  • FOUND commit a9860c8 (Task 2)
  • FOUND commit a7339ab (Task 3)