Każdy e-mail powiadomieniowy ma teraz:
(1) link w stopce "Wyłącz ten typ powiadomień jednym kliknięciem"
(2) nagłówki List-Unsubscribe + List-Unsubscribe-Post dla klientów
pocztowych (Gmail/Apple Mail pokażą natywny przycisk Unsubscribe)
Implementacja:
- utils/unsubscribe_tokens.py: signed token (itsdangerous, SECRET_KEY)
niosący user_id + notification_type, bez wygasania
- blueprints/unsubscribe: GET /unsubscribe?t=TOKEN → strona potwierdzenia,
POST /unsubscribe → faktyczne wyłączenie flagi notify_email_<type>
- email_service.send_email() dostał parametr notification_type. Jeśli
przekazany razem z user_id, footer + headery są doklejane
- Aktualizowane wywołania: message_notification (messages),
classified_question/answer (B2B Q&A), classified_expiry (skrypt cron)
Prefetch safety: GET pokazuje stronę z przyciskiem "Tak, wyłącz",
wyłączenie następuje po POST. RFC 8058 One-Click (POST bez formularza
z Content-Type application/x-www-form-urlencoded + body
"List-Unsubscribe=One-Click") obsługuje klientów pocztowych.
D.2/D.3 dorzucą kolejne notification_type (forum, broadcast, events).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MessageUploadService.__init__() requires app_root but was called
without arguments, causing send_message errors and double-sending
due to JS retry. Now uses current_app.root_path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The actual bug: Composer.send() appends message to DOM via appendMessage(),
then 1-5s later the polling loop fetches the same message from API and
appends it again. Fix: track sent messages in state.messages[convId] so
the polling dedup check (msg.id > newestId) filters them out.
Also removed debug logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>