nordabiz/docs/superpowers/specs/2026-03-27-messaging-redesign-design.md
Maciej Pienczyn 110d971dca
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
feat: migrate prod docs to OVH VPS + UTC→Warsaw timezone in all templates
Production moved from on-prem VM 249 (10.22.68.249) to OVH VPS
(57.128.200.27, inpi-vps-waw01). Updated ALL documentation, slash
commands, memory files, architecture docs, and deploy procedures.

Added |local_time Jinja filter (UTC→Europe/Warsaw) and converted
155 .strftime() calls across 71 templates so timestamps display
in Polish timezone regardless of server timezone.

Also includes: created_by_id tracking, abort import fix, ICS
calendar fix for missing end times, Pros Poland data cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:41:53 +02:00

190 lines
6.6 KiB
Markdown

# Messaging Redesign — Conversation-Based System
**Data:** 2026-03-27
**Status:** Zaakceptowany
**Zakres:** Przebudowa systemu wiadomości z email-like (Odebrane/Wysłane) na konwersacyjny (Messenger/WhatsApp)
## Decyzje architektoniczne
1. **Ujednolicony model** — 1:1 i grupy w jednym modelu `Conversation` + `Message`. Rozmowa 1:1 = konwersacja z 2 uczestnikami.
2. **Real-time: SSE** — Server-Sent Events + Redis pub/sub. Jedno połączenie SSE per użytkownik.
3. **Migracja danych** — istniejące wiadomości migrowane do nowego modelu. Stare tabele zostają jako backup.
## Model danych
### conversations
| Kolumna | Typ | Opis |
|---------|-----|------|
| id | Serial PK | |
| name | String(255), nullable | Null dla 1:1, nadana nazwa dla grup |
| is_group | Boolean | False = 1:1, True = grupa |
| owner_id | FK → users | Twórca |
| created_at | DateTime | |
| updated_at | DateTime | Aktualizowane przy każdej wiadomości |
| last_message_id | FK → messages, nullable | Denormalizacja dla listy |
### conversation_members
| Kolumna | Typ | Opis |
|---------|-----|------|
| conversation_id | FK → conversations, PK | |
| user_id | FK → users, PK | |
| role | String(20) | 'owner', 'member' |
| last_read_at | DateTime | Read receipts |
| is_muted | Boolean | Wyciszenie email + push |
| is_archived | Boolean | Ukrycie z listy |
| joined_at | DateTime | |
| added_by_id | FK → users, nullable | |
### messages
| Kolumna | Typ | Opis |
|---------|-----|------|
| id | Serial PK | |
| conversation_id | FK → conversations | |
| sender_id | FK → users | |
| content | Text | HTML (Quill) |
| reply_to_id | FK → messages, nullable | Cytowanie |
| edited_at | DateTime, nullable | |
| is_deleted | Boolean | Soft delete |
| link_preview | JSONB, nullable | {url, title, description, image} |
| created_at | DateTime | |
### message_reactions
| Kolumna | Typ | Opis |
|---------|-----|------|
| id | Serial PK | |
| message_id | FK → messages | |
| user_id | FK → users | |
| emoji | String(10) | |
| created_at | DateTime | |
| UNIQUE | (message_id, user_id, emoji) | |
### message_pins
| Kolumna | Typ | Opis |
|---------|-----|------|
| id | Serial PK | |
| conversation_id | FK → conversations | |
| message_id | FK → messages | |
| pinned_by_id | FK → users | |
| created_at | DateTime | |
### message_attachments
Istniejąca tabela. Nowa kolumna `new_message_id` FK → messages, nullable.
## SSE Real-time
### Endpoint
`GET /api/messages/stream` — jedno połączenie per użytkownik.
### Zdarzenia
| Event | Dane | Kiedy |
|-------|------|-------|
| new_message | conversation_id, message JSON | Nowa wiadomość |
| message_read | conversation_id, user_id, read_at | Przeczytano |
| typing | conversation_id, user_id, user_name | Ktoś pisze (TTL 3s) |
| reaction | message_id, user_id, emoji, action | Reakcja |
| message_edited | message_id, new_content, edited_at | Edycja |
| message_deleted | message_id, conversation_id | Usunięcie |
| message_pinned | message_id, conversation_id, pinned_by | Przypięcie |
| presence | user_id, status, last_seen | Online/offline |
### Infrastruktura
- Redis pub/sub do rozgłaszania między workerami Gunicorn
- Online status: Redis SETEX z TTL 60s, heartbeat co 30s
- Typing: POST /api/conversations/<id>/typing → Redis publish, TTL 3s
## API Endpoints
### Konwersacje
| Method | URL | Opis |
|--------|-----|------|
| GET | /wiadomosci | Widok konwersacyjny (HTML) |
| GET | /api/conversations | Lista konwersacji JSON |
| POST | /api/conversations | Nowa (deduplikacja 1:1) |
| GET | /api/conversations/<id> | Szczegóły + członkowie |
| PATCH | /api/conversations/<id> | Edytuj nazwę/opis |
| DELETE | /api/conversations/<id> | Usuń (owner) |
| POST | /api/conversations/<id>/members | Dodaj członka |
| DELETE | /api/conversations/<id>/members/<uid> | Usuń członka |
| PATCH | /api/conversations/<id>/settings | Mute/archive |
### Wiadomości
| Method | URL | Opis |
|--------|-----|------|
| GET | /api/conversations/<id>/messages | Paginacja cursor-based |
| POST | /api/conversations/<id>/messages | Wyślij |
| PATCH | /api/messages/<id> | Edytuj (swoje, max 24h) |
| DELETE | /api/messages/<id> | Soft delete (swoje) |
| POST | /api/messages/<id>/forward | Przekaż |
| POST | /api/conversations/<id>/read | Oznacz przeczytane |
| POST | /api/conversations/<id>/typing | Typing indicator |
### Reakcje i przypięcia
| Method | URL | Opis |
|--------|-----|------|
| POST | /api/messages/<id>/reactions | Dodaj |
| DELETE | /api/messages/<id>/reactions/<emoji> | Usuń |
| POST | /api/messages/<id>/pin | Przypnij |
| DELETE | /api/messages/<id>/pin | Odepnij |
| GET | /api/conversations/<id>/pins | Lista przypiętych |
### Inne
| Method | URL | Opis |
|--------|-----|------|
| GET | /api/messages/stream | SSE |
| GET | /api/users/presence | Online status (batch) |
| POST | /api/messages/upload | Upload pliku |
## Frontend
### Desktop
- Lewy panel (380px): lista konwersacji posortowana po updated_at
- Prawy panel: nagłówek (avatar, imię, status, typing) + wiadomości (bąbelki) + input (Quill)
### Wiadomości
- Bąbelki: moje (niebieskie, prawo) / cudze (szare, lewo)
- Separatory dat
- Reply-to: cytat nad odpowiedzią
- Edytowane: etykieta "(edytowano)"
- Usunięte: "Wiadomość usunięta"
- Załączniki inline (obrazy jako podgląd, pliki jako pill)
- Reakcje: pill badges pod bąbelkiem
- Link preview: karta z tytułem + opisem
- Read receipts 1:1: ptaszki (wysłano/doręczono/przeczytano), hover → timestampy
- Read receipts grupa: awatary (max 4 + "+N")
### Menu kontekstowe (hover/long-press)
Odpowiedz, Reaguj (6 emoji), Przekaż, Przypnij, Edytuj, Usuń
### Mobile (< 768px)
Lista LUB chat (nie oba). Przycisk "Wróć". Menu kontekstowe jako bottom sheet.
## Email notifications
```
if member.is_muted → nie wysyłaj
elif not user.notify_email_messages → nie wysyłaj
else → wysyłaj
```
Wyciszona konwersacja: ikona 🔇 na liście.
## Link preview
- Backend wykrywa URL, pobiera stronę (timeout 3s), parsuje og:title/og:description/og:image
- Fallback: <title> + <meta description>
- Tylko pierwszy URL, brak preview dla wewnętrznych linków
- Zapisane w messages.link_preview (JSONB)
## Migracja danych
1. Prywatne wiadomości: grupowanie po parach sender/recipient → conversation (is_group=False)
2. Grupy: message_group → conversation (is_group=True)
3. Załączniki: nowy FK new_message_id
4. Walidacja: count before = count after, read receipts zachowane
5. Stare tabele zostają jako backup
## Zależności infrastrukturalne
- Redis na VM produkcyjnej (pub/sub + presence cache)
- Nginx: SSE wymaga wyłączenia buforowania (`proxy_buffering off`) dla /api/messages/stream