# Changelog

## [Unreleased]

### Added

- **Channel auto-sync:** Per-channel **Ticket sync** dropdown (Odoo / Trello / Atlassian / GitHub). The first message on a new ticket creates the remote record once; later replies do not re-sync. Global credentials stay in Settings → Ticket integrations.
- **Ticket integrations (Odoo / Trello / Atlassian / GitHub):** Admin Settings → **Ticket integrations** (provider dropdown) stores credentials and config. Staff ticket detail shows **Sync this to …** for each enabled provider; a preview modal lets them edit title/description before the API creates the remote issue/card. Links stored in `ticket_integration_links`. Migration `029_ticket_integrations.sql`. API under `/api/v1/integrations/*`.
- **Calendar (day drill + WhatsApp confirm):** Month view drills into a day on click; day/week slot select creates a booking (contact + WhatsApp channel + assignee). Travel/prep shows as **In** / **Out**. Booked items can **Send & wait** for client YES/NO via a chosen WhatsApp channel (`POST /calendar/requests`, `/confirm-message`, `/client-confirm`; pending state in `extraction.client_confirm`). Compact calendar chrome.
- **Manage → Staff:** Directory at `/staff` (after Calendar) with **card / list** views. Staff detail is full-screen: WhatsApp chat is the main pane (history for that profile phone; inbound replies reuse the open direct ticket), with tickets / calendar / activity in a side rail. API: `GET /api/v1/users/staff`, `GET /api/v1/users/staff/:id` (includes `chat`), `POST /api/v1/users/staff/:id/chat`.
- **WhatsApp — Africa's Talking provider:** The Add WhatsApp channel flow chooses **Baileys / QR** or **Africa's Talking**. One deployment account (`AFRICASTALKING_USERNAME`, `AFRICASTALKING_API_KEY`, passed through both compose API services and both env examples); both must be non-empty before an Africa's Talking channel can be created; no per-channel secrets. Direct text plus image/voice/document media (outbound via hosted signed URL); templates indicated but no template admin. No groups, QR or session reconnect (Baileys only) — channel cards show provider and capability badges and disable those actions. Outbound media requires `SUPABASE_PUBLIC_URL` to be a publicly reachable HTTPS origin, since the provider fetches the signed URL from its own servers. Channel status now follows the last outbound interaction: an `authentication` provider error marks the channel `disconnected` and the next successful send restores `connected` (Baileys status stays owned by the worker). Because this provider has no Reconnect, a `disconnected` Africa's Talking channel **stays sendable** — staff send guards allow it, the ticket composer stays enabled with a warning, and the send itself revalidates the corrected deployment credentials; disconnected Baileys channels still block sends exactly as before. Inbound callback `/api/v1/inbound/whatsapp/africas-talking/:token` (token visible to admins only) accepts JSON **and** `application/x-www-form-urlencoded` (scoped parser, no new dependency), acknowledges delivery/status/read events with **200 `{ok:true, ignored:true, reason:"unsupported_event"}`** instead of 400, and is rate limited **per channel token at 600/min** (keyed on a digest of the token, not the caller IP), with 1 MiB body, a clamped 16 MiB media cap, four concurrent media downloads behind a four-deep queue with a 5s wait deadline (overflow and timeouts shed as **503** with `Retry-After: 5`, so a queued callback does not outlive the provider's redelivery window and trigger a wasted re-download plus a transient 500 from the racing `wa_message_id` unique violation), and HTTPS/SSRF checks that also reject the NAT64, 6to4, Teredo and IPv4-translated tunnel prefixes. DNS rebinding remains a documented residual risk. Migration `028_channel_whatsapp_provider.sql` (existing WhatsApp rows become `baileys`); rollback needs a maintenance procedure (back up, reassign tickets/calendar requests to a Baileys channel, remove AT channels, then drop schema and code together) documented in the README, not yet rehearsed. Real Africa's Talking callback and live credentials not yet exercised.
- **Admin dashboard — staff workload:** Active staff count, per-staff open ticket counts (including zeros), and unassigned open footer. **Admin-only** — `GET /api/v1/users` is admin-guarded; non-admin staff and portal users do not see this block. Live ticket queue shows status, assignee (or Unassigned), and last-activity relative time for all dashboard viewers.
- **Ticket API list/detail:** `assigned_user.display_name` and `last_message_at` (max message `created_at`; UI falls back to `last_message_at ?? updated_at ?? created_at`).
- **WhatsApp group vs 1:1:** Direct (DM) inbound never attaches to an open group ticket for the same contact — `findOpenTicket` excludes rows with `wa_group_jid`. Group messages still use `findOpenByGroupJid`.
- **WhatsApp group AI replies:** Group @tags that use WhatsApp **LID** (e.g. `@7409…`) count as mentioning the channel. Untagged follow-ups after the channel’s last question also trigger talkback (skips pure chatter like “lol”). Automated talkback sends into `wa_group_jid`, not the sender’s 1:1 chat.
- **WhatsApp group member management:** Group ticket right rail shows Group card (subject, channel, client, live members). Staff can **Add** (Support/Remote filter) or **Remove** staff; client cannot be removed. API: `GET/POST/DELETE /tickets/:id/group[/participants]`. Worker: `/group/metadata`, `/group/participants`.
- **WhatsApp group tickets:** New ticket toggle **Direct** / **WhatsApp group**. Group mode creates a Baileys group (client + 1–3 staff with `users.phone`), stores `wa_group_jid` on the ticket, syncs inbound group messages to that ticket, and sends replies to the group. Migration `024_ticket_wa_group.sql`. Staff picker: `GET /api/v1/users/staff-for-groups`. Group AI talkback only when @mentioned or clearly questioned.
- **Staff phone & tags:** Admin Users create/detail can set WhatsApp **phone** and tags **support** / **remote** (`users.staff_tags`, migration `025`). Group New ticket picker filters All / Support / Remote.
- **Staff New ticket (WhatsApp):** Topbar / Tickets **New ticket** opens a modal to pick a connected WhatsApp channel, existing contact or new phone, and send a required first message. Always creates a new ticket (`POST /api/v1/tickets`), assigns to the staff user, and sends via Baileys (delivery errors match existing reply behaviour).
- **Calendar (Manage):** Staff page at `/calendar` for call-out and meetup scheduling. Inbound WhatsApp/portal/email messages with date/time intent create **suggested** `calendar_requests` (LLM extraction); staff must **Book** or **Decline**. **Call-outs:** 30m travel before/after. **Teams / online:** 45m meeting, no travel, 15m prep after. Overlapping assignee slots return **409**. Migration `023_calendar_requests.sql`. API under `/api/v1/calendar/*`. Ticket detail shows a banner linking to the pending suggestion. Talkback acknowledges bookings (checks calendar availability preview) and asks **call-out vs Teams** when the type is unclear; relative times use Africa/Johannesburg.
- **User automation:** Expanded Python API sim (`user_automation/`) with personas `admin_operator`, `agent_handler`, `end_user`, `technical_user`, `email_reporter`, `landing_visitor`; staff bootstrap (companies / portal users / staff users); finite `--cycles` with JSON/MD reports under `user_automation/reports/` (or `--cycles 0` soak).
- **Landing page chat:** Login page left panel opens **support tickets only** (`POST /api/v1/landing/chat`) after matching company + work email to an existing subscription — **no passwords in chat**. Mismatch → **Request access**. Staff use the form on the right. Migration `022_landing_sessions.sql` (+ API bootstrap for existing DBs).
- **Channels — AI assistant:** Per-channel **AI offline message** (channel settings) — sent to customers when automated AI cannot connect, and shown to staff as the fallback reply suggestion.
- **Ticket chat (staff):** AI-generated reply suggestions above the composer — three draft replies from channel/company knowledge; click to fill the reply box, refresh to regenerate (`POST /api/v1/tickets/:id/reply-suggestions`).
- **Docs:** `docs/whatsapp_media_audit.md` — WhatsApp ↔ ticket media flow trace, Trello checklist status, gaps table, and recommended build order (images/voice Milestone 1 + open items for documents, failed-send UI, AI placeholders).
- **Migration `020_message_document_type.sql`:** `message_content_type` enum value `document`.

### Changed

- **Sidebar:** Channels moved to **Settings** (Manage channels link). **Knowledge base** moved under **Admin** (admin-only route).
- **Dashboard:** Open tickets KPI links to `/tickets?status=open`. Admin top row includes **Active users** (currently up). Staff dashboard shows **Today's calendar** booked slots.

- **Tickets / Dashboard:** WhatsApp group tickets show a blue **Group** badge in the Tickets list and Dashboard live queue (title uses `wa_group_subject` when set).
- **Ticket chat right rail (staff):** Ticket details stacks Status / Priority / Channel / Opened vertically with the priority control moved into that card (badge beside the title). Customer card shows company account code + AI autoreply, with an **Earlier tickets** accordion (up to 2 prior tickets: status, channel, assignee).
- **Ticket detail:** Failed WhatsApp outbound messages show a delivery error on the bubble (`message:sent` socket + missing `wa_message_id`). Disconnected WhatsApp channels show a banner, disable the composer, and update live via `channel:connected` / `channel:disconnected`.
- **Ticket media — documents:** Inbound WhatsApp `documentMessage` (PDF, Office, zip, etc.) stored and shown as download links; staff/portal can attach documents outbound on WhatsApp tickets. Requires migration `020` on existing DBs.
- **Ticket media preview:** PDF and text attachments open in an in-app preview dialog; other document types still download-only.
- **Docs:** Refreshed `docs/whatsapp_media_audit.md` (Trello closure status, open backlog vs shipped, verification steps); updated `whatsapp_media_milestone1.md` (AI, delivery UX, MIME lists).

### Fixed

- **Landing page chat database access:** The API no longer runs owner-only index DDL while opening a session. Migration `026_landing_sessions_api_privileges.sql` grants the `postgres` API role only `SELECT`, `INSERT`, and `UPDATE` on migration-owned `landing_sessions`.
- **Africa's Talking callbacks:** Delivery and status receipts that echo the original message type are acknowledged as non-message events instead of returning 400; callbacks with actual text or media content remain messages.
- **Ticket realtime:** After API reconnect, ticket chat re-joins Socket.IO rooms; login refreshes the WS JWT; new messages also emit a global `ticket:updated` so the tickets list refreshes live.
- **Talkback vision:** Inbound WhatsApp images are sent to OpenAI-compatible agents as multimodal `image_url` parts (was text-only `[image]`, so GPT-5 replied it could not view images).
- **Talkback voice notes:** Inbound WhatsApp voice notes are transcribed via OpenAI Whisper before talkback; transcript is stored on the message and shown in the ticket UI.
- **Ticket media / WhatsApp images:** Dev `.env` `ANON_KEY` / `SERVICE_ROLE_KEY` must be JWTs signed with `JWT_SECRET` (mismatched keys caused Storage `invalid signature` and inbound images never appeared on tickets). `SUPABASE_PUBLIC_URL` aligned to Kong host port.
- **Channels reconnect QR:** Reconnect no longer reuses stale Baileys auth from the `wa_auth` volume (silent re-login → “Logged out” with no QR). Worker now clears per-channel auth on reconnect / logout so a fresh QR is emitted; QR modal shows a timeout message if none arrives.
- **Ticket chat (staff):** AI reply suggestions no longer fail when the model returns plain text — transcript-based prompt, OpenAI JSON mode, and tolerant parsing (JSON object/array, numbered lists, or single plain-text draft).
- **Docker / Supabase:** Moved Kong config, Postgres init SQL (`_supabase`, roles, Logflare schema, etc.), Vector config, and Edge Functions bootstrap from an uncommitted `volumes/` tree into **`docker/supabase/`** so fresh clones initialize Postgres correctly (fixes `database "_supabase" does not exist`, missing role passwords, and Kong `Is a directory` on Windows when bind-mount sources were absent).
- **GoTrue:** `GOTRUE_JWT_EXP` now reads **`GOTRUE_JWT_EXP`** (seconds) instead of **`JWT_EXPIRY`**, so `.env` values like `8h` no longer crash auth.
- **PostgREST:** `PGRST_APP_SETTINGS_JWT_EXP` uses **`POSTGREST_JWT_EXP`** (seconds) for the same reason.
- **Kong:** `KONG_PLUGINS` includes **`request-termination`** and **`post-function`** so the declarative config from upstream Supabase loads on **`kong:2.8.1`**.
- **Frontend (Docker):** Strip CRLF from **`docker-entrypoint.sh`** during the image build so Alpine’s **`exec`** does not fail on Windows checkouts.
- **Docs:** PRD embedded compose paths aligned with **`docker/supabase/`** and current Kong / Logflare env.
- **Ticket media:** API rewrites Storage signed URLs from internal **`http://kong:8000`** to **`SUPABASE_PUBLIC_URL`** (derived from **`PUBLIC_SITE_HOST` + `KONG_HTTP_HOST_PORT`**) so chat images/audio load in the browser on dev, QA, and production.
- **Ticket chat images:** Click a thumbnail to open a full-screen lightbox with zoom (scroll / buttons) and drag-to-pan when zoomed.
