# Messaging System Enhancements — Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add 6 incremental improvements to the `/wiadomosci` messaging system — flash messages, form hints, read receipts, recipient preview, branded email template, and file attachments. **Architecture:** Each feature is independent and deployed as a separate commit. Features 1-4 are pure template/route changes requiring no DB migrations. Feature 5 refactors email sending into the branded template service. Feature 6 introduces a new `MessageAttachment` model with file upload support. **Tech Stack:** Flask 3.0, SQLAlchemy 2.0, Jinja2, PostgreSQL, Microsoft Graph API (email), vanilla JS **Spec:** `docs/superpowers/specs/2026-03-11-messaging-enhancements-design.md` --- ## Chunk 1: Features 1-3 (Flash, Hint, Read Receipts) ### Task 1: Post-Send Flash Message **Files:** - Modify: `blueprints/messages/routes.py:226` (messages_send flash) - Modify: `blueprints/messages/routes.py:319` (messages_reply flash) - [ ] **Step 1: Update flash in messages_send()** Replace line 226 in `blueprints/messages/routes.py`: ```python # OLD: flash('Wiadomość wysłana.', 'success') # NEW: if recipient.notify_email_messages != False and recipient.email: flash('Wiadomość wysłana! Odbiorca zostanie powiadomiony emailem.', 'success') else: flash('Wiadomość wysłana!', 'success') ``` Note: The `recipient` variable is already available in scope (line 155). The email notification check (line 197) uses the same condition, so this is consistent. - [ ] **Step 2: Update flash in messages_reply()** Replace line 319 in `blueprints/messages/routes.py`. The reply route needs access to the recipient user object — `original` is already queried (line 289), and `recipient_id` is computed (line 298), but the actual `User` object isn't loaded. Add a query: ```python # After line 308 (after block_exists check), add: recipient = db.query(User).filter(User.id == recipient_id).first() # Then replace line 319: # OLD: flash('Odpowiedź wysłana.', 'success') # NEW: if recipient and recipient.notify_email_messages != False and recipient.email: flash('Odpowiedź wysłana! Odbiorca zostanie powiadomiony emailem.', 'success') else: flash('Odpowiedź wysłana!', 'success') ``` Note: `User` import is already at the top of the file (line 5). The recipient query is also needed later for Feature 5 (email notification in reply). - [ ] **Step 3: Test manually on dev** Run: `python3 app.py` 1. Send a message to a user with `notify_email_messages=True` — verify flash says "...powiadomiony emailem" 2. Send a message to a user with `notify_email_messages=False` — verify flash says only "Wiadomość wysłana!" 3. Reply to a message — verify same conditional behavior - [ ] **Step 4: Commit** ```bash git add blueprints/messages/routes.py git commit -m "feat(messages): add conditional post-send flash with email notification info" ``` --- ### Task 2: Contextual Hint on Compose Form **Files:** - Modify: `templates/messages/compose.html:357` (after form-actions div, before ``) - [ ] **Step 1: Add hint text to compose.html** In `templates/messages/compose.html`, add hint text after the `` on line 357 (closing `.form-actions`) and before `` on line 358: ```html
📧 Odbiorca zostanie powiadomiony o nowej wiadomości emailem
``` - [ ] **Step 2: Verify visually on dev** Run: `python3 app.py` Navigate to `/wiadomosci/nowa` — verify the hint text appears below the submit button, styled as muted secondary text. - [ ] **Step 3: Commit** ```bash git add templates/messages/compose.html git commit -m "feat(messages): add email notification hint on compose form" ``` --- ### Task 3: Read Receipts in Sent Box **Files:** - Modify: `templates/messages/sent.html:128-131` (CSS) and `templates/messages/sent.html:211-213` (indicator) - [ ] **Step 1: Update CSS for read receipts** In `templates/messages/sent.html`, replace the `.read-status` CSS block (lines 128-131): ```css /* OLD: */ .read-status { font-size: var(--font-size-xs); color: var(--success); } /* NEW: */ .read-status { font-size: var(--font-size-xs); display: inline-flex; align-items: center; gap: 4px; } .read-status.is-read { color: var(--primary); } .read-status.is-unread { color: var(--text-secondary); } .read-status svg { width: 14px; height: 14px; } ``` - [ ] **Step 2: Update read indicator in message list** Replace lines 211-213 in `templates/messages/sent.html`: ```html {% if msg.is_read %}
Przeczytana
{% endif %} {% if msg.is_read %}
Przeczytana
{% else %}
Wysłana
{% endif %} ``` The double checkmark SVG (two overlapping check paths) indicates read, single checkmark indicates sent. The `title` attribute on read messages shows the `read_at` timestamp on hover. No new DB queries needed — `is_read` and `read_at` are already on the `PrivateMessage` model (database.py:2273-2274). - [ ] **Step 3: Verify on dev** Run: `python3 app.py` 1. Navigate to `/wiadomosci/wyslane` — verify all sent messages show either single gray checkmark "Wysłana" or double blue checkmark "Przeczytana" 2. Hover over read messages — verify `read_at` timestamp appears as tooltip - [ ] **Step 4: Commit** ```bash git add templates/messages/sent.html git commit -m "feat(messages): add read receipt indicators in sent box" ``` --- ## Chunk 2: Feature 4 (Recipient Profile Preview) ### Task 4: Extend User Query with Company Data **Files:** - Modify: `blueprints/messages/routes.py:96-100` (messages_new user query) - [ ] **Step 1: Add LEFT JOIN to Company** Replace lines 96-100 in `blueprints/messages/routes.py`. The current query: ```python users = db.query(User).filter( User.is_active == True, User.is_verified == True, User.id != current_user.id ).order_by(User.name).all() ``` Replace with: ```python from sqlalchemy import func from database import UserCompanyPermissions, Company # Query users with their primary company info users_with_companies = db.query( User, Company.name.label('company_name'), Company.slug.label('company_slug'), UserCompanyPermissions.position.label('position') ).outerjoin( UserCompanyPermissions, (UserCompanyPermissions.user_id == User.id) ).outerjoin( Company, (Company.id == UserCompanyPermissions.company_id) & (Company.status == 'active') ).filter( User.is_active == True, User.is_verified == True, User.id != current_user.id ).order_by(User.name).all() # Deduplicate users (one user may have multiple company permissions) seen_ids = set() users = [] for user, company_name, company_slug, position in users_with_companies: if user.id not in seen_ids: seen_ids.add(user.id) user._company_name = company_name user._company_slug = company_slug user._position = position users.append(user) ``` Note: `UserCompanyPermissions` and `Company` may need importing at the top of the file. Check existing imports first — `Company` may already be imported. The `func` import from sqlalchemy is for safety but not strictly needed here. - [ ] **Step 2: Verify query returns data on dev** Run `python3 app.py`, navigate to `/wiadomosci/nowa`, open browser dev tools → check the users JSON in page source to confirm company data fields will be present (after Step 3). - [ ] **Step 3: Commit query change** ```bash git add blueprints/messages/routes.py git commit -m "feat(messages): extend user query with company data for recipient preview" ``` --- ### Task 5: Add Preview Card to Compose Template **Files:** - Modify: `templates/messages/compose.html` (user JSON, preview card HTML, JS) - [ ] **Step 1: Extend user JSON in template** In `templates/messages/compose.html`, update the users JS array (lines 366-369). Change from: ```javascript {id: {{ user.id }}, name: {{ (user.name or user.email.split('@')[0]) | tojson }}, email: {{ user.email | tojson }}, showEmail: {{ 'true' if user.privacy_show_email != False else 'false' }}}{{ ',' if not loop.last }} ``` To: ```javascript {id: {{ user.id }}, name: {{ (user.name or user.email.split('@')[0]) | tojson }}, email: {{ user.email | tojson }}, showEmail: {{ 'true' if user.privacy_show_email != False else 'false' }}, companyName: {{ (user._company_name or '') | tojson }}, companySlug: {{ (user._company_slug or '') | tojson }}, position: {{ (user._position or '') | tojson }}}{{ ',' if not loop.last }} ``` - [ ] **Step 2: Add preview card HTML** In `templates/messages/compose.html`, add preview card HTML after the `#recipient-selected` div. Find the recipient-selected div (around line 330-335) and add below it: ```html ``` - [ ] **Step 3: Update selectRecipient() JS function** In the `selectRecipient` function (line 408), add preview card population. After `searchInput.value = '';` (line 416), add: ```javascript // Show recipient preview card var user = users.find(function(u) { return u.id === id; }); var previewDiv = document.getElementById('recipient-preview'); if (user && (user.companyName)) { document.getElementById('preview-avatar').textContent = (name || email)[0].toUpperCase(); document.getElementById('preview-name').textContent = name; var companyHtml = ''; if (user.companyName) { companyHtml = user.companySlug ? '' + user.companyName + '' : user.companyName; } if (user.position) { companyHtml = user.position + (companyHtml ? ' · ' + companyHtml : ''); } document.getElementById('preview-company').innerHTML = companyHtml; previewDiv.style.display = 'block'; } else { previewDiv.style.display = 'none'; } ``` - [ ] **Step 4: Update clearRecipient() to hide preview** In `clearRecipient()` (line 419), add after `searchInput.focus();` (line 424): ```javascript document.getElementById('recipient-preview').style.display = 'none'; ``` - [ ] **Step 5: Verify on dev** Run `python3 app.py`, navigate to `/wiadomosci/nowa`: 1. Type a user name in the autocomplete 2. Select a user — verify preview card appears with avatar, name, and company 3. Click "x" to clear — verify preview card disappears 4. Select a user without company — verify no preview card shown - [ ] **Step 6: Commit** ```bash git add templates/messages/compose.html git commit -m "feat(messages): add recipient profile preview card on compose form" ``` --- ## Chunk 3: Feature 5 (Branded Email Template) ### Task 6: Create Branded Email Helper **Files:** - Modify: `email_service.py` (new helper function after `_email_v3_wrap`) - [ ] **Step 1: Add helper function to email_service.py** After the `_email_v3_wrap()` function (after line 381), add: ```python def build_message_notification_email(sender_name: str, subject: str, content_preview: str, message_url: str, settings_url: str) -> tuple: """Build branded email for message notification. Returns (html, text).""" subject_html = f'

Temat: {subject}

' if subject else '' content_html = f'''

{sender_name} wysłał(a) Ci wiadomość na portalu Norda Biznes.

{subject_html}

{content_preview}

Przeczytaj wiadomość

Możesz wyłączyć powiadomienia e-mail w ustawieniach prywatności.

''' html = _email_v3_wrap('Nowa wiadomość', f'od {sender_name}', content_html) text = f'{sender_name} wysłał(a) Ci wiadomość na portalu Norda Biznes. Odczytaj: {message_url}' return html, text ``` - [ ] **Step 2: Commit helper** ```bash git add email_service.py git commit -m "feat(email): add branded message notification email builder" ``` --- ### Task 7: Replace Inline HTML in messages_send() **Files:** - Modify: `blueprints/messages/routes.py:197-224` (email sending block in messages_send) - [ ] **Step 1: Import the new helper** At the top of `blueprints/messages/routes.py`, add to existing imports: ```python from email_service import send_email, build_message_notification_email ``` If `send_email` is already imported, just add `build_message_notification_email`. - [ ] **Step 2: Replace inline HTML email block** Replace lines 197-224 in `blueprints/messages/routes.py`: ```python # Send email notification if recipient has it enabled if recipient.notify_email_messages != False and recipient.email: try: message_url = url_for('.messages_view', message_id=message.id, _external=True) settings_url = url_for('auth.konto_prywatnosc', _external=True) sender_name = current_user.name or current_user.email.split('@')[0] preview = (content[:200] + '...') if len(content) > 200 else content subject_line = f'Nowa wiadomość od {sender_name} — Norda Biznes' email_html, email_text = build_message_notification_email( sender_name=sender_name, subject=subject, content_preview=preview, message_url=message_url, settings_url=settings_url ) send_email( to=[recipient.email], subject=subject_line, body_text=email_text, body_html=email_html, email_type='message_notification', user_id=recipient.id, recipient_name=recipient.name ) except Exception as e: import logging logging.getLogger(__name__).warning(f"Failed to send message email notification: {e}") ``` - [ ] **Step 3: Verify email on dev** Run `python3 app.py`, send a message. Check email (or logs) to verify the branded template is used — should show gradient header with Norda Biznes logo, styled content, CTA button, footer with address. - [ ] **Step 4: Commit** ```bash git add blueprints/messages/routes.py git commit -m "feat(messages): use branded email template for message notifications" ``` --- ### Task 8: Add Email Notification + UserNotification to messages_reply() **Files:** - Modify: `blueprints/messages/routes.py:309-317` (messages_reply after reply creation) The `messages_reply()` route currently does NOT send email notifications or create UserNotification. This is a gap — the reply sender's recipient doesn't know they got a reply unless they manually check the inbox. - [ ] **Step 1: Add imports if needed** Ensure these imports exist at the top of `blueprints/messages/routes.py`: ```python from database import UserNotification # may already be imported ``` - [ ] **Step 2: Add flush, notification, and email to messages_reply()** Replace lines 316-317 in `blueprints/messages/routes.py`: ```python # OLD: db.add(reply) db.commit() # NEW: db.add(reply) db.flush() # Get reply.id for notification link # Create in-app notification sender_name = current_user.name or current_user.email.split('@')[0] notification = UserNotification( user_id=recipient_id, title=f'Nowa odpowiedź od {sender_name}', message=f'{sender_name} odpowiedział(a) na wiadomość' + (f': {original.subject}' if original.subject else ''), notification_type='message', related_type='message', related_id=reply.id, action_url=url_for('.messages_view', message_id=message_id) ) db.add(notification) # Send email notification if recipient and recipient.notify_email_messages != False and recipient.email: try: message_url = url_for('.messages_view', message_id=message_id, _external=True) settings_url = url_for('auth.konto_prywatnosc', _external=True) preview = (content[:200] + '...') if len(content) > 200 else content subject_line = f'Nowa odpowiedź od {sender_name} — Norda Biznes' email_html, email_text = build_message_notification_email( sender_name=sender_name, subject=f"Re: {original.subject}" if original.subject else None, content_preview=preview, message_url=message_url, settings_url=settings_url ) send_email( to=[recipient.email], subject=subject_line, body_text=email_text, body_html=email_html, email_type='message_notification', user_id=recipient_id, recipient_name=recipient.name ) except Exception as e: import logging logging.getLogger(__name__).warning(f"Failed to send reply email notification: {e}") db.commit() ``` Note: The `recipient` variable was added in Task 1 Step 2 (queried after block_exists check). Make sure that step is done first. - [ ] **Step 3: Test reply notification on dev** 1. Send a message, then reply as the other user 2. Verify: in-app notification created (check bell icon), email sent with branded template 3. Reply to a message where recipient has `notify_email_messages=False` — verify no email sent - [ ] **Step 4: Commit** ```bash git add blueprints/messages/routes.py git commit -m "feat(messages): add email notification and in-app notification for replies" ``` --- ## Chunk 4: Feature 6 — File Attachments (Model + Migration) ### Task 9: Create MessageAttachment Model **Files:** - Modify: `database.py` (add new model after PrivateMessage class, line ~2287) - [ ] **Step 1: Add MessageAttachment model** After line 2287 in `database.py` (after the `PrivateMessage` class relationships), add: ```python class MessageAttachment(Base): """Załączniki do wiadomości prywatnych""" __tablename__ = 'message_attachments' id = Column(Integer, primary_key=True) message_id = Column(Integer, ForeignKey('private_messages.id', ondelete='CASCADE'), nullable=False) filename = Column(String(255), nullable=False) # original filename stored_filename = Column(String(255), nullable=False) # UUID-based on disk file_size = Column(Integer, nullable=False) # bytes mime_type = Column(String(100), nullable=False) created_at = Column(DateTime, default=datetime.now) message = relationship('PrivateMessage', backref=backref('attachments', cascade='all, delete-orphan')) ``` Also add `backref` to the import from `sqlalchemy.orm` at the top of `database.py` if not already there. - [ ] **Step 2: Verify model loads** Run: `python3 -c "from database import MessageAttachment; print('OK')"` — should print "OK". - [ ] **Step 3: Commit** ```bash git add database.py git commit -m "feat(messages): add MessageAttachment model" ``` --- ### Task 10: Create SQL Migration **Files:** - Create: `database/migrations/063_message_attachments.sql` - [ ] **Step 1: Write migration SQL** ```sql -- Migration: 063_message_attachments.sql -- Description: Create message_attachments table for file attachments in messaging -- Date: 2026-03-11 CREATE TABLE IF NOT EXISTS message_attachments ( id SERIAL PRIMARY KEY, message_id INTEGER NOT NULL REFERENCES private_messages(id) ON DELETE CASCADE, filename VARCHAR(255) NOT NULL, stored_filename VARCHAR(255) NOT NULL, file_size INTEGER NOT NULL, mime_type VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_message_attachments_message_id ON message_attachments(message_id); -- Permissions GRANT ALL ON TABLE message_attachments TO nordabiz_app; GRANT USAGE, SELECT ON SEQUENCE message_attachments_id_seq TO nordabiz_app; ``` - [ ] **Step 2: Check migration number** ```bash ls database/migrations/ | tail -5 ``` Verify 063 doesn't conflict. If the last migration is 063 or higher, increment accordingly. - [ ] **Step 3: Run migration on dev** ```bash python3 scripts/run_migration.py database/migrations/063_message_attachments.sql ``` - [ ] **Step 4: Commit** ```bash git add database/migrations/063_message_attachments.sql git commit -m "feat(messages): add message_attachments migration" ``` --- ## Chunk 5: Feature 6 — File Attachments (Upload Service + Routes) ### Task 11: Create Message Upload Service **Files:** - Create: `message_upload_service.py` This service extends the `FileUploadService` pattern from `file_upload_service.py` but supports both images and documents. - [ ] **Step 1: Create message_upload_service.py** ```python """File upload service for message attachments.""" import os import uuid import struct from datetime import datetime from werkzeug.utils import secure_filename # Allowed file types and their limits IMAGE_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'} DOCUMENT_EXTENSIONS = {'pdf', 'docx', 'xlsx'} ALLOWED_EXTENSIONS = IMAGE_EXTENSIONS | DOCUMENT_EXTENSIONS MAX_IMAGE_SIZE = 5 * 1024 * 1024 # 5MB per image MAX_DOCUMENT_SIZE = 10 * 1024 * 1024 # 10MB per document MAX_TOTAL_SIZE = 15 * 1024 * 1024 # 15MB total per message MAX_FILES_PER_MESSAGE = 3 # Magic bytes signatures for validation FILE_SIGNATURES = { 'jpg': [b'\xff\xd8\xff'], 'jpeg': [b'\xff\xd8\xff'], 'png': [b'\x89PNG\r\n\x1a\n'], 'gif': [b'GIF87a', b'GIF89a'], 'pdf': [b'%PDF'], 'docx': [b'PK\x03\x04'], # ZIP-based format 'xlsx': [b'PK\x03\x04'], # ZIP-based format } MIME_TYPES = { 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif', 'pdf': 'application/pdf', 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', } UPLOAD_BASE = 'static/uploads/messages' class MessageUploadService: """Handles file uploads for message attachments.""" def __init__(self, app_root: str): self.app_root = app_root def validate_files(self, files: list) -> tuple: """Validate list of uploaded files. Returns (valid_files, errors).""" errors = [] valid = [] if len(files) > MAX_FILES_PER_MESSAGE: errors.append(f'Maksymalnie {MAX_FILES_PER_MESSAGE} pliki na wiadomość.') return [], errors total_size = 0 for f in files: if not f or not f.filename: continue filename = secure_filename(f.filename) ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else '' if ext not in ALLOWED_EXTENSIONS: errors.append(f'Niedozwolony typ pliku: {filename}') continue # Read file content for size + magic bytes check content = f.read() f.seek(0) size = len(content) max_size = MAX_IMAGE_SIZE if ext in IMAGE_EXTENSIONS else MAX_DOCUMENT_SIZE if size > max_size: limit_mb = max_size // (1024 * 1024) errors.append(f'Plik {filename} przekracza limit {limit_mb}MB.') continue total_size += size # Magic bytes validation if ext in FILE_SIGNATURES: valid_sig = False for sig in FILE_SIGNATURES[ext]: if content[:len(sig)] == sig: valid_sig = True break if not valid_sig: errors.append(f'Plik {filename} ma nieprawidłowy format.') continue valid.append((f, filename, ext, size, content)) if total_size > MAX_TOTAL_SIZE: errors.append(f'Łączny rozmiar plików przekracza {MAX_TOTAL_SIZE // (1024*1024)}MB.') return [], errors return valid, errors def save_file(self, content: bytes, ext: str) -> tuple: """Save file to disk. Returns (stored_filename, relative_path).""" now = datetime.now() subdir = os.path.join(UPLOAD_BASE, str(now.year), f'{now.month:02d}') full_dir = os.path.join(self.app_root, subdir) os.makedirs(full_dir, exist_ok=True) stored_filename = f'{uuid.uuid4().hex}.{ext}' full_path = os.path.join(full_dir, stored_filename) with open(full_path, 'wb') as out: out.write(content) relative_path = os.path.join(subdir, stored_filename) return stored_filename, relative_path def get_mime_type(self, ext: str) -> str: """Get MIME type for extension.""" return MIME_TYPES.get(ext, 'application/octet-stream') def is_image(self, ext: str) -> bool: """Check if extension is an image type.""" return ext in IMAGE_EXTENSIONS def delete_file(self, relative_path: str): """Delete a file from disk.""" full_path = os.path.join(self.app_root, relative_path) if os.path.exists(full_path): os.remove(full_path) ``` - [ ] **Step 2: Verify service loads** Run: `python3 -c "from message_upload_service import MessageUploadService; print('OK')"` - [ ] **Step 3: Commit** ```bash git add message_upload_service.py git commit -m "feat(messages): add message upload service for file attachments" ``` --- ### Task 12: Add File Upload to messages_send() **Files:** - Modify: `blueprints/messages/routes.py` (messages_send route) - [ ] **Step 1: Import upload service and model** At the top of `blueprints/messages/routes.py`, add: ```python from message_upload_service import MessageUploadService from database import MessageAttachment ``` - [ ] **Step 2: Add file processing to messages_send()** After `db.flush()` (line 178, which gets `message.id`) and before the UserNotification creation (line ~180), add file processing: ```python # Process file attachments attachment_errors = [] if request.files.getlist('attachments'): import os upload_service = MessageUploadService(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) files = [f for f in request.files.getlist('attachments') if f and f.filename] if files: valid_files, attachment_errors = upload_service.validate_files(files) if attachment_errors: db.rollback() for err in attachment_errors: flash(err, 'error') return redirect(url_for('.messages_new')) for f, filename, ext, size, content in valid_files: stored_filename, relative_path = upload_service.save_file(content, ext) attachment = MessageAttachment( message_id=message.id, filename=filename, stored_filename=stored_filename, file_size=size, mime_type=upload_service.get_mime_type(ext) ) db.add(attachment) ``` - [ ] **Step 3: Similarly add to messages_reply()** In `messages_reply()`, after `db.flush()` (added in Task 8) and before the UserNotification creation, add the same file processing block but using `reply.id` instead of `message.id`: ```python # Process file attachments if request.files.getlist('attachments'): import os upload_service = MessageUploadService(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) files = [f for f in request.files.getlist('attachments') if f and f.filename] if files: valid_files, attachment_errors = upload_service.validate_files(files) if attachment_errors: db.rollback() for err in attachment_errors: flash(err, 'error') return redirect(url_for('.messages_view', message_id=message_id)) for f, filename, ext, size, content in valid_files: stored_filename, relative_path = upload_service.save_file(content, ext) attachment = MessageAttachment( message_id=reply.id, filename=filename, stored_filename=stored_filename, file_size=size, mime_type=upload_service.get_mime_type(ext) ) db.add(attachment) ``` - [ ] **Step 4: Commit** ```bash git add blueprints/messages/routes.py git commit -m "feat(messages): process file attachments on send and reply" ``` --- ## Chunk 6: Feature 6 — File Attachments (Templates) ### Task 13: Add File Input to Compose Form **Files:** - Modify: `templates/messages/compose.html` - [ ] **Step 1: Add enctype to form tag** Find the ` ``` - [ ] **Step 2: Add file input before form-actions** Before the `.form-actions` div (line 348), add: ```html

Przeciągnij pliki tutaj lub wybierz z dysku

JPG, PNG, GIF (5MB) · PDF, DOCX, XLSX (10MB)

``` - [ ] **Step 3: Add file handling JS** In the `{% block extra_js %}` section, add file handling JavaScript (at the end of the existing IIFE or as a new block): ```javascript // File attachment handling (function() { var dropZone = document.getElementById('file-drop-zone'); var fileInput = document.getElementById('file-input'); var fileList = document.getElementById('file-list'); if (!dropZone) return; dropZone.addEventListener('click', function(e) { if (e.target.tagName !== 'A') fileInput.click(); }); dropZone.addEventListener('dragover', function(e) { e.preventDefault(); dropZone.style.borderColor = 'var(--primary)'; }); dropZone.addEventListener('dragleave', function() { dropZone.style.borderColor = 'var(--border-color)'; }); dropZone.addEventListener('drop', function(e) { e.preventDefault(); dropZone.style.borderColor = 'var(--border-color)'; var dt = new DataTransfer(); Array.from(e.dataTransfer.files).forEach(function(f) { dt.items.add(f); }); Array.from(fileInput.files).forEach(function(f) { dt.items.add(f); }); fileInput.files = dt.files; updateFileList(); }); fileInput.addEventListener('change', updateFileList); function updateFileList() { var files = Array.from(fileInput.files); if (files.length === 0) { fileList.innerHTML = ''; return; } fileList.innerHTML = files.map(function(f, i) { var sizeMB = (f.size / 1024 / 1024).toFixed(1); return '
' + '📎 ' + '' + f.name + ' ' + '(' + sizeMB + ' MB) ' + '' + '
'; }).join(''); } window.removeFile = function(index) { var dt = new DataTransfer(); Array.from(fileInput.files).forEach(function(f, i) { if (i !== index) dt.items.add(f); }); fileInput.files = dt.files; updateFileList(); }; })(); ``` - [ ] **Step 4: Commit** ```bash git add templates/messages/compose.html git commit -m "feat(messages): add file upload input to compose form" ``` --- ### Task 14: Add File Input to Reply Form **Files:** - Modify: `templates/messages/view.html:233-239` (reply form) - [ ] **Step 1: Add enctype and file input to reply form** Replace lines 233-239 in `templates/messages/view.html`: ```html
Maks. 3 pliki, 15MB łącznie
``` - [ ] **Step 2: Commit** ```bash git add templates/messages/view.html git commit -m "feat(messages): add file upload to reply form" ``` --- ### Task 15: Display Attachments in Message View **Files:** - Modify: `templates/messages/view.html:226-227` (after message body) - Modify: `blueprints/messages/routes.py` (eager load attachments in messages_view) - [ ] **Step 1: Eager load attachments in messages_view()** In `messages_view()` route, update the message query to eager load attachments. Find the query line and add `options(joinedload(...))`: ```python from sqlalchemy.orm import joinedload # In messages_view(), update the message query: message = db.query(PrivateMessage).options( joinedload(PrivateMessage.attachments) ).filter( PrivateMessage.id == message_id ).first() ``` Note: Also check if `joinedload` is already imported. - [ ] **Step 2: Add attachment display in view.html** After line 226 (`
{{ message.content }}
`) in `templates/messages/view.html`, add: ```html {% if message.attachments %}

Załączniki ({{ message.attachments|length }})

{% for att in message.attachments %} {% set is_image = att.mime_type.startswith('image/') %}
{% if is_image %} {{ att.filename }} {% else %} 📄
{{ att.filename }} {{ (att.file_size / 1024)|round(0)|int }} KB
{% endif %}
{% endfor %}
{% endif %} ``` - [ ] **Step 3: Test on dev** 1. Send a message with image attachment — verify inline thumbnail in view 2. Send a message with PDF — verify file icon with download link 3. Verify clicking image opens full size in new tab 4. Verify clicking document downloads with original filename - [ ] **Step 4: Commit** ```bash git add templates/messages/view.html blueprints/messages/routes.py git commit -m "feat(messages): display attachments in message view" ``` --- ### Task 16: Add Attachment Indicator to Inbox and Sent Box **Files:** - Modify: `templates/messages/inbox.html` (paperclip icon) - Modify: `templates/messages/sent.html` (paperclip icon) - Modify: `blueprints/messages/routes.py` (eager load in inbox/sent queries) - [ ] **Step 1: Eager load attachments in messages_inbox()** In `messages_inbox()` route, add `options(joinedload(PrivateMessage.attachments))` to the query. - [ ] **Step 2: Eager load attachments in messages_sent()** In `messages_sent()` route, add `options(joinedload(PrivateMessage.attachments))` to the query. - [ ] **Step 3: Add paperclip icon to inbox.html** In `templates/messages/inbox.html`, find the message-preview div and add after it: ```html {% if msg.attachments %} 📎 {{ msg.attachments|length }} {% endif %} ``` - [ ] **Step 4: Add paperclip icon to sent.html** Same pattern in `templates/messages/sent.html` — add after the message-preview div. - [ ] **Step 5: Commit** ```bash git add templates/messages/inbox.html templates/messages/sent.html blueprints/messages/routes.py git commit -m "feat(messages): add attachment indicator in inbox and sent box" ``` --- ## Chunk 7: Final Testing & Deployment ### Task 17: Full Integration Test on Dev - [ ] **Step 1: Start dev server** ```bash python3 app.py ``` - [ ] **Step 2: Test all 6 features end-to-end** 1. **Flash message:** Send message → verify conditional flash text 2. **Hint:** Compose form → verify email hint below button 3. **Read receipts:** Sent box → verify checkmark icons and tooltips 4. **Recipient preview:** Select recipient → verify mini business card 5. **Branded email:** Send message → verify branded email received 6. **Attachments:** Send with image + PDF → verify upload, display, download - [ ] **Step 3: Test edge cases** 1. Send message without attachments (backward compat) 2. Send with 4+ files → verify error 3. Send with oversized file → verify error 4. Reply with attachment → verify works 5. Mobile viewport → verify file upload zone works ### Task 18: Deploy to Staging - [ ] **Step 1: Push to remotes** ```bash git push origin master && git push inpi master ``` - [ ] **Step 2: Deploy to staging** ```bash ssh maciejpi@10.22.68.248 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes" ``` - [ ] **Step 3: Run migration on staging** ```bash ssh maciejpi@10.22.68.248 "cd /var/www/nordabiznes && /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/063_message_attachments.sql" ``` - [ ] **Step 4: Verify on staging** ```bash curl -sI https://staging.nordabiznes.pl/health | head -3 ``` Test messaging features manually on staging. ### Task 19: Deploy to Production - [ ] **Step 1: Deploy** ```bash ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull" ``` - [ ] **Step 2: Run migration** ```bash ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/063_message_attachments.sql" ``` - [ ] **Step 3: Create upload directory** ```bash ssh maciejpi@57.128.200.27 "mkdir -p /var/www/nordabiznes/static/uploads/messages && sudo chown -R maciejpi:maciejpi /var/www/nordabiznes/static/uploads/messages" ``` - [ ] **Step 4: Restart and verify** ```bash ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes" curl -sI https://nordabiznes.pl/health | head -3 ``` - [ ] **Step 5: Update release notes** Add new entries to `release_notes` in `blueprints/public/routes.py` following the existing style (user-facing language, no technical jargon).