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 fromname + optional category, derivesidvia 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 withid,name,categoryfields.POST /admin/resetβ extended with optionalchannel_idquery 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 callingingest_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
- Slug derivation server-side only β client posts
name, server computesid. Eliminates client/server id mismatch. - UniqueViolation before Exception β
except psycopg2.errors.UniqueViolationmust appear beforeexcept Exceptionin create_channel. Duplicate names produce 409, not 500. re.fullmatchnotre.matchβre.matchonly anchors at start;re.fullmatchrequires the entire slug to match, preventing trailing invalid chars from slipping through.except HTTPException: raiseguard in ingest β without this, the 404 for missing channel would be caught byexcept Exceptionand re-raised as 500.- 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)