# Feedback Protocol (Frontend <-> Backend) This document defines the canonical backend contract for user feedback submission. Protocol verification status: - Backend route source: `backend/src/v1/feedback/router.py` - Backend schema source: `backend/src/v1/feedback/schemas.py` - Backend service source: `backend/src/v1/feedback/service.py` - Backend dependencies source: `backend/src/v1/feedback/dependencies.py` - Backend model source: `backend/src/models/user_feedback.py` - Backend report source: `backend/src/v1/feedback/report.py` - Backend tasks source: `backend/src/v1/feedback/tasks.py` - Backend email source: `backend/src/core/email/sender.py` - Frontend mapping source: `apps/lib/features/settings/data/apis/feedback_api.dart` - Storage config source: `backend/src/core/config/settings.py` - Current status: Phase 1 + Phase 2 + Phase 3 implemented ## Compatibility strategy - Current strategy: additive evolution (`backward-compatible`). - Breaking change requires explicit migration + rollback notes (`requires-migration`). ## Route overview - Submit feedback: `POST /api/v1/feedback` (multipart/form-data) ## Auth and trust boundary - Feedback submission supports both authenticated and anonymous modes. - Authenticated: `Authorization: Bearer {token}` header. Backend extracts `user_id` from JWT. - Anonymous: No `Authorization` header. `user_id` stored as `NULL`. - Frontend controls anonymity via checkbox ("Do not upload my personal information"). ## Submit feedback ### Request ``` POST /api/v1/feedback Content-Type: multipart/form-data Authorization: Bearer {token} # Optional Form Fields: - feedback_type: "bug" | "suggestion" | "other" (required) - content: string (required, 1-500 chars after trim) - device_info: string (JSON, e.g. {"platform":"ios","model":"iPhone 15"}) - app_version: string (required) - os_version: string (required) Files: - images: File[] (optional, max 3 files, jpg/png only, max 5MB each) ``` ### Response 201 ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-04-17T10:30:00+00:00" } ``` ### Error codes | code | status | meaning | |---|---:|---| | `FEEDBACK_CONTENT_EMPTY` | 400 | Content is empty or whitespace-only | | `FEEDBACK_CONTENT_TOO_LONG` | 400 | Content exceeds 500 characters | | `FEEDBACK_TOO_MANY_IMAGES` | 400 | More than 3 images uploaded | | `FEEDBACK_IMAGE_TOO_LARGE` | 400 | Single image exceeds 5MB | | `FEEDBACK_INVALID_IMAGE_TYPE` | 400 | Image type not jpg/png | | `FEEDBACK_SUBMIT_FAILED` | 500 | Internal storage or database failure | | `REQUEST_VALIDATION_ERROR` | 422 | Invalid feedback_type or device_info JSON | ## Database schema Table: `user_feedback` | Column | Type | Nullable | Default | Description | |---|---|---|---|---| | `id` | UUID | NO | `gen_random_uuid()` | Primary key | | `user_id` | UUID | YES | - | FK to auth.users, NULL = anonymous | | `feedback_type` | VARCHAR(20) | NO | `'other'` | bug/suggestion/other | | `content` | TEXT | NO | - | Feedback content, max 500 chars | | `images` | JSONB | NO | `'[]'` | Storage path list, max 3 | | `device_info` | JSONB | NO | `'{}'` | Device info JSON | | `app_version` | VARCHAR(20) | NO | - | App version string | | `os_version` | VARCHAR(50) | NO | - | OS version string | | `status` | VARCHAR(20) | NO | `'pending'` | pending/processed | | `created_at` | TIMESTAMPTZ | NO | `now()` | Creation timestamp | | `updated_at` | TIMESTAMPTZ | NO | `now()` | Last update timestamp | ## Storage - Bucket: configured via `ERYAO_STORAGE__FEEDBACK__BUCKET` (default: `feedback-images`) - Visibility: private (no public access needed) - Path pattern: `{YYYY-MM-DD}/{timestamp}_{index}.{ext}` - Max file size: configured via `ERYAO_STORAGE__FEEDBACK__MAX_SIZE_MB` (default: 5) - Allowed types: `image/jpeg`, `image/png` ## Frontend implementation notes - Uses `image_picker` package for image selection - Uses `dio` `FormData` for multipart upload - Anonymous mode: uses `Options(extra: {'skipAuth': true})` to bypass token injection in ApiClient interceptor - Client-side validation: content required, max 500 chars, max 3 images - Server-side validation: all field validation happens in `FeedbackService` ## Phase 2: Report Generation (Implemented) - **Report format**: xlsx (openpyxl) with embedded images - **Image columns**: Screenshots anchored to columns K/L/M - **Report path**: `{log_dir}/reports/{YYYYMMDD_HHMMSS}_feedback_report.xlsx` - **Status transition**: `pending` -> `processed` after report generation ## Phase 3: Email Delivery (Implemented) - **Email service**: `core/email/sender.py` (aiosmtplib, stateless) - **SMTP provider**: Feishu enterprise email (`smtp.feishu.cn:465`) - **Templates**: `core/email/templates/feedback/` - `daily_report.html`: HTML email with stats cards (sent when feedbacks exist) - `no_feedback.html`: Plain email (sent when no feedbacks in time range) - **Attachment naming**: `{YYYYMMDD_HHMMSS}_feedback_report.xlsx` - **Schedule**: Configured via `ERYAO_FEEDBACK_REPORT__CRON` (default: `0 10 * * *`) - **Enable flag**: `ERYAO_FEEDBACK_REPORT__ENABLED` (default: `false`) ### Email configuration ```bash ERYAO_EMAIL__HOST=smtp.feishu.cn ERYAO_EMAIL__PORT=465 ERYAO_EMAIL__USE_SSL=true ERYAO_EMAIL__USERNAME= ERYAO_EMAIL__PASSWORD= ERYAO_EMAIL__FROM_ADDRESS= ERYAO_EMAIL__FROM_NAME=Eryao Feedback System ERYAO_FEEDBACK_REPORT__EMAIL= ``` ### Email template variables | Variable | Description | |---|---| | `${start_date}` | Report period start date (YYYY-MM-DD) | | `${start_hour}` | Report period start hour | | `${end_date}` | Report period end date (YYYY-MM-DD) | | `${end_hour}` | Report period end hour | | `${total_count}` | Total feedback count | | `${bug_count}` | Bug type count | | `${suggestion_count}` | Suggestion type count | | `${generated_at}` | Report generation timestamp |