# Denya OneCare — Agent memory Denya OneCare is a centralized maintenance/issue tracking system for Pavilion Accra (FastAPI + SQLite + Alpine.js + Twilio). ## Quick start ```bash # Start the app docker-compose up # Or directly uvicorn app.main:app --reload ``` ## Project structure ``` app/ ├── core/ # config, database, security (JWT + bcrypt + RBAC) ├── models/ # SQLAlchemy ORM models ├── schemas/ # Pydantic request/response schemas ├── services/ # Business logic (auth, seed, ticket, sla) └── routers/ # FastAPI route handlers alembic/ # Database migrations tests/ # pytest suite; conftest.py swaps DATABASE_URL to a temp SQLite uploads/ # Photo uploads (created at runtime) ``` ## Commands - `alembic upgrade head` — apply migrations - `alembic revision --autogenerate -m "msg"` — new migration - `pytest` — run the API test suite (tests/; pagination contract anchored in tests/test_tickets_pagination.py) ## Seed data Users, units, and categories are auto-seeded on first startup via lifespan hook: - 17 users covering all roles (Admin/Jerome, Admin/Wahab, CS Rep, CS Manager, FM Dispatcher, Tech, CEO, Director) - 120 apartment units (East/West, 10 floors × 6 apts per wing) - Default password for all seed users: `denya123` - Units load from `apartment_mapping.json` if present, else built-in fallback - Categories: 23 top-level (13 Maintenance, 6 CS, 4 Emergency) with sub-categories, seeded from `app/services/seed.py::SEED_CATEGORIES_DATA` ## Key API endpoints ### Auth & Health | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/health` | No | Health check | | POST | `/api/auth/login` | No | Get JWT tokens (rate-limited ~5 fails/15min/IP+email → 429) | | POST | `/api/auth/refresh` | Token | Refresh tokens | | GET | `/api/auth/me` | Bearer | Current user | | GET | `/api/auth/admin-only` | Admin/Jerome, Admin/Wahab | RBAC demo endpoint | | GET | `/api/auth/users` | Bearer | List users (id, name, role) for the assign-technician picker | | POST | `/api/auth/users` | Admin/Jerome, Admin/Wahab | Admin creates a user (forced canonical role; unknown roles → 422) | | PATCH | `/api/auth/users/{id}` | Admin/Jerome, Admin/Wahab | Role change / deactivate (self-modification → 400) | | DELETE | `/api/auth/users/{id}` | Admin/Jerome, Admin/Wahab | Delete user (409 if referenced by tickets/timeline/escalations) | **Self-registration is removed** — `POST /api/auth/register` 404s and there is no sign-up UI; users are created/managed by admins only (P0 hardening batch). ### WhatsApp | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/api/whatsapp/webhook` | No | Meta handshake (`hub.verify_token`, constant-time; mismatch → 403) | | POST | `/api/whatsapp/webhook` | `X-Webhook-Secret` header | Inbound message → ticket + log. Fail-closed: 403 when `WHATSAPP_WEBHOOK_SECRET` is unset or the header doesn't match | | GET | `/api/whatsapp/mock-log` | Bearer | Recent webhook submissions (debug; auth required) | `whatsapp_log` predates the real Meta webhook (the model gained `message_text`/`wa_message_id`/`ticket_number` and dropped `command` without a migration), so legacy tables keep the old `(command, …)` shape and every ORM read/write 500s. `ensure_legacy_schema` (app/main.py) adds the missing columns and backfills+drops the obsolete NOT NULL `command` column idempotently at startup — do not hand-edit legacy DBs, ship a self-heal there instead. ### Pages (Sprint 3) — Jinja2 templates at `app/templates/` | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/login` | No | Login page | | GET | `/dashboard/cs` | Client | CS dashboard | | GET | `/dashboard/fm` | Client | FM dashboard | | GET | `/dashboard/ceo` | Client | CEO dashboard | | GET | `/tickets` | Client | All Issues filterable table | | GET | `/tickets/new` | Client | Create Issue form | | GET | `/tickets/{id}` | Client | Issue detail with timeline | Frontend: Alpine.js + Tailwind CSS vendored same-origin (no CDN) — see "Frontend assets" below. Auth state in localStorage. Role-based nav routing in `base.html`. ### Frontend assets (vendored, LAN-safe) - Alpine.js 3.17.2 + Tailwind Play 3.4.17 are committed under `app/static/vendor/` and served at `/static/vendor/…` (mounted in `app/main.py`, versioned filenames → immutable cache `public, max-age=31536000, immutable`). Templates must never reference a CDN; update `app/templates/base.html` when upgrading: download `alpinejs@/dist/cdn.min.js` (jsDelivr) and the tailwind play script (`cdn.tailwindcss.com/`), save them under `app/static/vendor/` mirroring the committed names (Alpine keeps `.min.js`, e.g. `alpine-3.17.2.min.js`; the tailwind play file does not, e.g. `tailwind-3.4.17.js`), then bump the `