diff --git a/docs/plans/2026-02-22-debug-cli-design.md b/docs/plans/2026-02-22-debug-cli-design.md new file mode 100644 index 0000000..69f96a1 --- /dev/null +++ b/docs/plans/2026-02-22-debug-cli-design.md @@ -0,0 +1,106 @@ +# Debug CLI Design + +**Date:** 2026-02-22 +**Status:** Approved + +## Goal + +Add a debug CLI that mirrors all web UI interactions, with superuser access to impersonate any user. Purpose: debug flows without authorization overhead. + +## Decisions + +- **Scope:** All API endpoints (listings, decisions, POIs, tasks, districts) +- **Execution mode:** Switchable — direct service calls (default) or HTTP to running API (`--http`) +- **User identity:** `--user-email` flag to impersonate any user +- **Location:** New `cli/` package with submodules, registered under a `debug` group in `main.py` +- **Output:** Pretty-printed by default, `--json` flag for machine-readable output + +## Structure + +``` +cli/ + __init__.py # Exports all groups for registration in main.py + _context.py # Shared context: user identity, HTTP vs direct, output formatting + listings.py # list, detail, stream, refresh + decisions.py # set, list, remove + pois.py # create, list, update, delete, calculate, distances + tasks.py # status, list, cancel, clear + districts.py # list +``` + +### Usage + +```bash +python main.py debug --user-email user@example.com listings list --type RENT --min-bedrooms 2 +python main.py debug --user-email user@example.com --http decisions set 12345 liked --type RENT +python main.py debug --user-email user@example.com --json pois list +``` + +## Shared Context (`_context.py`) + +- **`CliContext` dataclass:** Holds `user_email`, `use_http`, `json_output`, `api_base_url` +- **`get_user()`:** Creates `User(sub=email, email=email, name=email)` from provided email — no token validation +- **Direct mode:** Instantiates repositories and calls service functions, passing the fake `User` +- **HTTP mode:** Mints a self-signed JWT using `JWT_SECRET` from env (passkey-style token), makes requests via `httpx` +- **Output helpers:** `output(data, json_mode)` — prints JSON or pretty-formatted text + +## Command Mapping + +### `listings` group + +| Command | Direct mode | HTTP mode | +|---------|------------|-----------| +| `list` | `listing_service.get_listings()` | `GET /api/listing` | +| `detail ` | Repository + poi_service | `GET /api/listing/{id}/detail` | +| `stream` | `export_service.export_to_geojson()` | `GET /api/listing_geojson/stream` | +| `refresh` | `listing_service.refresh_listings()` | `POST /api/refresh_listings` | + +### `decisions` group + +| Command | Direct mode | HTTP mode | +|---------|------------|-----------| +| `set ` | `decision_service.set_decision()` | `PUT /api/decisions/{id}` | +| `list` | `decision_service.get_user_decisions()` | `GET /api/decisions` | +| `remove ` | `decision_service.remove_decision()` | `DELETE /api/decisions/{id}` | + +### `pois` group + +| Command | Direct mode | HTTP mode | +|---------|------------|-----------| +| `list` | `poi_service.get_user_pois()` | `GET /api/poi` | +| `create` | `poi_service.create_poi()` | `POST /api/poi` | +| `update ` | `poi_service.update_poi()` | `PUT /api/poi/{id}` | +| `delete ` | `poi_service.delete_poi()` | `DELETE /api/poi/{id}` | +| `calculate ` | `poi_service.trigger_calculation()` | `POST /api/poi/{id}/calculate` | +| `distances` | `poi_service.get_distances_for_listing()` | `GET /api/poi/distances` | + +### `tasks` group + +| Command | Direct mode | HTTP mode | +|---------|------------|-----------| +| `status ` | `task_service.get_task_status()` | `GET /api/task_status` | +| `list` | `task_service.get_user_tasks()` | `GET /api/tasks_for_user` | +| `cancel ` | `task_service.cancel_task()` | `POST /api/cancel_task` | +| `clear` | `task_service.clear_all_tasks()` | `POST /api/clear_all_tasks` | + +### `districts` group + +| Command | Direct mode | HTTP mode | +|---------|------------|-----------| +| `list` | `district_service.get_all_districts()` | `GET /api/get_districts` | + +## Error Handling + +- **Direct mode:** Catch service exceptions, print readable messages (or `{"error": "..."}` in JSON mode) +- **HTTP mode:** Print status code + response body on non-2xx +- **Missing services:** Fail fast with clear message about which service (DB, Redis) is unavailable + +## Testing + +- Unit tests for `_context.py` (JWT minting, user creation, output formatting) +- Integration tests for representative commands in direct mode (mocked repositories) +- No HTTP-mode tests (that's API testing, already covered) + +## Dependencies + +No new dependencies. Uses existing: `click`, `httpx`, `pyjwt`.