diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..60829bc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,25 @@ +# reklamator Development Guidelines + +Auto-generated from all feature plans. Last updated: 2025-10-15 + +## Active Technologies +- Python 3.11+ + Flask (web framework), no CSS frameworks, no JavaScript libraries (001-build-an-application) + +## Project Structure +``` +backend/ +frontend/ +tests/ +``` + +## Commands +cd src [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] pytest [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] ruff check . + +## Code Style +Python 3.11+: Follow standard conventions + +## Recent Changes +- 001-build-an-application: Added Python 3.11+ + Flask (web framework), no CSS frameworks, no JavaScript libraries + + + \ No newline at end of file diff --git a/specs/001-build-an-application/contracts/admin_api.md b/specs/001-build-an-application/contracts/admin_api.md new file mode 100644 index 0000000..7429f96 --- /dev/null +++ b/specs/001-build-an-application/contracts/admin_api.md @@ -0,0 +1,604 @@ +# Admin API Contract + +**Scope**: Product and user management endpoints (User Story P4) +**Authentication**: Required (admin role only) + +--- + +## GET /admin/products + +Display list of all registered products. + +### Request + +**Authentication**: Required (admin role) + +### Response + +**Success (200 OK)**: +```html +Content-Type: text/html + + + + Product Management + +

Product Management

+ + + Create New Product + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDNameStatusSubmission URLTarget LanguageFeedback CountAssigned OwnersActions
001-acme-appAcme Mobile AppActive/submit/acme-appEnglish (en)1272 owners + Edit | + Archive +
+ + +``` + +**Error (403 Forbidden)**: User is not an administrator +```html +Content-Type: text/html + + + + +

Access Denied

+

Administrator privileges required.

+ + +``` + +### Functional Requirements Covered +- FR-045: List all products +- FR-054: Display product statistics + +--- + +## GET /admin/products/new + +Display form to create a new product. + +### Request + +**Authentication**: Required (admin role) + +### Response + +**Success (200 OK)**: +```html +Content-Type: text/html + + + + +

Create New Product

+
+ + + + + + + + + + + + + +
+ + +``` + +### Functional Requirements Covered +- FR-045: Form to register new products +- FR-047: Set preferred target language +- FR-048: Assign product owners + +--- + +## POST /admin/products + +Create a new product. + +### Request + +**Authentication**: Required (admin role) + +**Form Data**: +- `id` (string, required): Unique product identifier (URL-safe, lowercase, hyphens allowed) +- `name` (string, required): Display name (1-100 characters) +- `description` (string, optional): Description (max 500 characters) +- `target_language` (string, required): ISO 639-1 language code +- `submission_url_slug` (string, required): URL-safe slug (unique) +- `assigned_owner_ids` (string[], required): At least one product owner ID + +### Response + +**Success (302 Redirect)**: +```http +HTTP/1.1 302 Found +Location: /admin/products +``` + +**Error (400 Bad Request)**: Validation failure +```html +Content-Type: text/html + + + + +

Validation Error

+ + + +``` + +### Side Effects + +1. **File System**: + - Creates `data/products/{product_id}/` + - Writes `data/products/{product_id}/config.yaml` + - Creates `data/products/{product_id}/feedback/` directory + +2. **Config File** (`config.yaml`): +```yaml +id: "001-acme-app" +name: "Acme Mobile App" +description: "Customer feedback for Acme's flagship mobile application" +target_language: "en" +submission_url_slug: "acme-app" +created_date: "2025-10-15" +status: "active" +assigned_owner_ids: + - "owner-001" + - "owner-002" +statistics: + total_feedback_count: 0 + last_submission: null +``` + +### Functional Requirements Covered +- FR-045: Register new products +- FR-046: Unique product identifier +- FR-047: Set target language +- FR-048: Assign product owners +- FR-049: Generate unique submission URL + +--- + +## GET /admin/products/{product_id}/edit + +Display form to edit an existing product. + +### Request + +**Authentication**: Required (admin role) + +**Path Parameters**: +- `product_id` (string, required): Product identifier + +### Response + +**Success (200 OK)**: Same form as create, pre-populated with existing values + +**Error (404 Not Found)**: Product does not exist + +### Functional Requirements Covered +- FR-051: Update product details + +--- + +## POST /admin/products/{product_id} + +Update an existing product. + +### Request + +**Authentication**: Required (admin role) + +**Path Parameters**: +- `product_id` (string, required): Product identifier + +**Form Data**: Same as POST /admin/products (except `id` is immutable) + +### Response + +**Success (302 Redirect)**: +```http +HTTP/1.1 302 Found +Location: /admin/products +``` + +**Error (400 Bad Request)**: Validation failure +**Error (404 Not Found)**: Product does not exist + +### Side Effects + +- Updates `data/products/{product_id}/config.yaml` +- Product `id` cannot be changed (immutable) +- Changing `target_language` affects future feedback translations only (FR-048) + +### Functional Requirements Covered +- FR-048: Update product owner assignments +- FR-051: Update product details + +--- + +## POST /admin/products/{product_id}/archive + +Archive a product (stop accepting new feedback). + +### Request + +**Authentication**: Required (admin role) + +**Path Parameters**: +- `product_id` (string, required): Product identifier + +### Response + +**Success (302 Redirect)**: +```http +HTTP/1.1 302 Found +Location: /admin/products +``` + +**Error (404 Not Found)**: Product does not exist + +### Side Effects + +- Updates `data/products/{product_id}/config.yaml`: Sets `status: "archived"` +- Submission form (GET /submit/{product_slug}) returns 404 for archived products (FR-053) +- Historical feedback preserved (FR-052) + +### Functional Requirements Covered +- FR-052: Archive products without deleting feedback +- FR-053: Prevent new submissions to archived products + +--- + +## POST /admin/products/{product_id}/unarchive + +Reactivate an archived product. + +### Request + +**Authentication**: Required (admin role) + +**Path Parameters**: +- `product_id` (string, required): Product identifier + +### Response + +**Success (302 Redirect)**: +```http +HTTP/1.1 302 Found +Location: /admin/products +``` + +### Side Effects + +- Updates `data/products/{product_id}/config.yaml`: Sets `status: "active"` +- Submission form becomes available again + +### Functional Requirements Covered +- Allow reversing archive operation (not explicitly in FR but useful) + +--- + +## GET /admin/users + +Display list of all users (product owners and admins). + +### Request + +**Authentication**: Required (admin role) + +### Response + +**Success (200 OK)**: +```html +Content-Type: text/html + + + + +

User Management

+ + + Create New User + + + + + + + + + + + + + + + + + + + + + + + + + +
IDEmailNameRoleAssigned ProductsLast LoginActions
owner-001jane.smith@example.comJane SmithProduct Owner2 products2025-10-15 09:23 + Edit | + Delete +
+ + +``` + +### Functional Requirements Covered +- User management interface (implied by FR-048: assigning owners) + +--- + +## GET /admin/users/new + +Display form to create a new user. + +### Request + +**Authentication**: Required (admin role) + +### Response + +**Success (200 OK)**: +```html +Content-Type: text/html + + + + +

Create New User

+
+ + + + + + + + + +
+ + +``` + +--- + +## POST /admin/users + +Create a new user. + +### Request + +**Authentication**: Required (admin role) + +**Form Data**: +- `email` (string, required): Valid email address (unique) +- `name` (string, required): Display name (1-100 characters) +- `password` (string, required): Password (min 8 characters) +- `role` (string, required): "product_owner" or "admin" + +### Response + +**Success (302 Redirect)**: +```http +HTTP/1.1 302 Found +Location: /admin/users +``` + +**Error (400 Bad Request)**: Validation failure +```html +Content-Type: text/html + + + + +

Validation Error

+ + + +``` + +### Side Effects + +- Appends new user to `data/users.yaml` +- Password hashed with bcrypt (cost factor 12) before storage (FR-063) +- Generates unique user ID (e.g., "owner-001", "admin-002") + +### Functional Requirements Covered +- FR-063: Secure password storage (bcrypt) +- User creation for product owner assignment + +--- + +## GET /admin/users/{user_id}/edit + +Display form to edit an existing user. + +### Request + +**Authentication**: Required (admin role) + +**Path Parameters**: +- `user_id` (string, required): User identifier + +### Response + +**Success (200 OK)**: Same form as create, pre-populated (except password field empty) + +**Error (404 Not Found)**: User does not exist + +--- + +## POST /admin/users/{user_id} + +Update an existing user. + +### Request + +**Authentication**: Required (admin role) + +**Path Parameters**: +- `user_id` (string, required): User identifier + +**Form Data**: +- `email` (string, required): Valid email address +- `name` (string, required): Display name +- `password` (string, optional): New password (if changing) +- `role` (string, required): "product_owner" or "admin" + +### Response + +**Success (302 Redirect)**: +```http +HTTP/1.1 302 Found +Location: /admin/users +``` + +### Side Effects + +- Updates user entry in `data/users.yaml` +- If password provided, re-hash with bcrypt +- Email and role can be updated + +--- + +## POST /admin/users/{user_id}/delete + +Delete a user. + +### Request + +**Authentication**: Required (admin role) + +**Path Parameters**: +- `user_id` (string, required): User identifier + +### Response + +**Success (302 Redirect)**: +```http +HTTP/1.1 302 Found +Location: /admin/users +``` + +**Error (400 Bad Request)**: Cannot delete self +```html +Content-Type: text/html + + + + +

Cannot Delete

+

You cannot delete your own account.

+ + +``` + +### Side Effects + +- Removes user from `data/users.yaml` +- User automatically unassigned from all products +- Historical feedback metadata unchanged (no user tracking in feedback) + +--- + +## Access Control + +All admin endpoints enforce: +1. User must be authenticated (session cookie) +2. User role must be "admin" +3. Otherwise: 403 Forbidden response + +### Functional Requirements Covered +- FR-045: Admin can register products +- FR-048: Admin can assign product owners +- FR-051: Admin can update products +- FR-052: Admin can archive products diff --git a/specs/001-build-an-application/contracts/dashboard_api.md b/specs/001-build-an-application/contracts/dashboard_api.md new file mode 100644 index 0000000..5bde73d --- /dev/null +++ b/specs/001-build-an-application/contracts/dashboard_api.md @@ -0,0 +1,420 @@ +# Dashboard API Contract + +**Scope**: Product owner dashboard endpoints (User Story P3) +**Authentication**: Required (session-based via Flask-Login) + +--- + +## GET /login + +Display login form for product owners and administrators. + +### Request + +**Headers**: None required + +**Query Parameters**: +- `next` (string, optional): Redirect URL after successful login + +### Response + +**Success (200 OK)**: +```html +Content-Type: text/html + + + + Login - Reklamator + +

Login

+
+ + + +
+ + +``` + +**Already Authenticated (302 Redirect)**: Redirect to `/dashboard` + +--- + +## POST /login + +Authenticate product owner or administrator. + +### Request + +**Headers**: +- `Content-Type: application/x-www-form-urlencoded` + +**Form Data**: +- `email` (string, required): User email +- `password` (string, required): User password + +### Response + +**Success (302 Redirect)**: +```http +HTTP/1.1 302 Found +Location: /dashboard +Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax +``` + +**Error (401 Unauthorized)**: +```html +Content-Type: text/html + + + + +

Login Failed

+

Invalid email or password.

+ + +``` + +### Functional Requirements Covered +- FR-056: Authentication required for dashboard +- FR-063: Password verification against bcrypt hash + +--- + +## GET /logout + +Log out current user. + +### Request + +**Authentication**: Required (session cookie) + +### Response + +**Success (302 Redirect)**: +```http +HTTP/1.1 302 Found +Location: /login +Set-Cookie: session=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT +``` + +--- + +## GET /dashboard + +Display product owner dashboard with feedback list. + +### Request + +**Authentication**: Required (session cookie) + +**Query Parameters**: +- `page` (integer, optional, default=1): Page number for pagination +- `category` (string, optional): Filter by category (idea/feature_request/bug/complaint) +- `language` (string, optional): Filter by original language (ISO 639-1 code) +- `status` (string, optional): Filter by status (analyzed/reviewed/in_progress/resolved/rejected) +- `date_from` (string, optional): Filter by date range start (ISO 8601 date) +- `date_to` (string, optional): Filter by date range end (ISO 8601 date) +- `search` (string, optional): Keyword search across text/translation/summary + +### Response + +**Success (200 OK)**: +```html +Content-Type: text/html + + + + Feedback Dashboard + +

Feedback Dashboard

+ + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IDDateCategoryOriginal LangSummaryStatusAttachments
a3f2c1d52025-10-15 14:32BugDEUser reports app crashes when uploading large files...Reviewed2 files
+ + + + + +``` + +**Error (401 Unauthorized)**: Not authenticated +```http +HTTP/1.1 302 Found +Location: /login?next=/dashboard +``` + +**Error (403 Forbidden)**: User has no assigned products +```html +Content-Type: text/html + + + + +

No Access

+

You are not assigned to any products.

+ + +``` + +### Behavior + +- Display only feedback for products assigned to current user (FR-033) +- Admin users see all products +- Default sort: newest first (FR-041) +- Pagination: 50 items per page (SC-008: <3s for 1000 items) +- Filters preserved in URL for sharing/bookmarking + +### Functional Requirements Covered +- FR-032: Authenticated dashboard access +- FR-033: Product owner access control +- FR-034: Display all analysis results +- FR-036-FR-041: Filtering and searching +- FR-041: Reverse chronological order + +--- + +## GET /feedback/{feedback_id} + +Display detailed view of a single feedback item. + +### Request + +**Authentication**: Required (session cookie) + +**Path Parameters**: +- `feedback_id` (UUID, required): Feedback identifier + +### Response + +**Success (200 OK)**: +```html +Content-Type: text/html + + + + Feedback Detail - a3f2c1d5 + +

Feedback Detail

+ +
+

ID: a3f2c1d5-8b4e-4f1a-9c2d-7e6f5a4b3c2d

+

Product: Acme Mobile App

+

Submitted: 2025-10-15 14:32:10 UTC

+

Original Language: German (DE)

+

Category: Bug (confidence: 0.92)

+

Status: +

+ + +
+

+
+ +

AI Analysis Summary

+

User reports that the app crashes when uploading large files. This appears to be a bug affecting the file upload module, preventing users from submitting documents over 5MB.

+ +

Original Text (German)

+
Die App stürzt ab, wenn ich versuche, große Dateien hochzuladen. Jedes Mal wenn ich eine PDF über 5MB hochlade, friert die App ein und schließt sich.
+ +

Translation (English)

+
The app crashes when I try to upload large files. Every time I upload a PDF over 5MB, the app freezes and closes.
+ +

Attachments

+ + + +``` + +**Error (401 Unauthorized)**: Not authenticated +```http +HTTP/1.1 302 Found +Location: /login?next=/feedback/{feedback_id} +``` + +**Error (403 Forbidden)**: User not authorized for this product +```html +Content-Type: text/html + + + + +

Access Denied

+

You do not have permission to view this feedback.

+ + +``` + +**Error (404 Not Found)**: Feedback does not exist +```html +Content-Type: text/html + + + + +

Feedback Not Found

+ + +``` + +### Functional Requirements Covered +- FR-034: Display complete feedback details +- FR-035: Links to download attachments +- FR-042: Status indicators +- FR-044: File attachments with icons/thumbnails + +--- + +## POST /feedback/{feedback_id}/status + +Update the status of a feedback item. + +### Request + +**Authentication**: Required (session cookie) + +**Path Parameters**: +- `feedback_id` (UUID, required): Feedback identifier + +**Form Data**: +- `status` (string, required): New status (analyzed/reviewed/in_progress/resolved/rejected) + +### Response + +**Success (302 Redirect)**: +```http +HTTP/1.1 302 Found +Location: /feedback/{feedback_id} +``` + +**Error (403 Forbidden)**: User not authorized for this product +**Error (404 Not Found)**: Feedback does not exist + +### Side Effects + +- Updates `metadata.yaml`: `status` field +- Preserves timestamp of status change + +### Functional Requirements Covered +- FR-042: Mark feedback with status indicators +- FR-043: Preserve status when filtering + +--- + +## GET /feedback/{feedback_id}/attachment/{filename} + +Download or view an attached file. + +### Request + +**Authentication**: Required (session cookie) + +**Path Parameters**: +- `feedback_id` (UUID, required): Feedback identifier +- `filename` (string, required): Sanitized filename + +### Response + +**Success (200 OK)**: Image file +```http +HTTP/1.1 200 OK +Content-Type: image/png +Content-Disposition: inline; filename="screenshot.png" +Content-Length: 245678 + +[binary image data] +``` + +**Success (200 OK)**: Document file +```http +HTTP/1.1 200 OK +Content-Type: application/pdf +Content-Disposition: attachment; filename="report.pdf" +Content-Length: 1234567 + +[binary document data] +``` + +**Error (403 Forbidden)**: User not authorized for this product +**Error (404 Not Found)**: File does not exist + +### Security + +- Files served via Flask route (not direct filesystem access per FR-058) +- Access control enforced: User must have access to parent product +- Path traversal prevention: Filename sanitized +- MIME type from stored metadata (not client-provided) + +### Functional Requirements Covered +- FR-035: Provide links to download/view attachments +- FR-058: Prevent unauthorized file access + +--- + +## Rate Limiting + +Dashboard endpoints are NOT rate limited (authenticated users only). diff --git a/specs/001-build-an-application/contracts/submission_api.md b/specs/001-build-an-application/contracts/submission_api.md new file mode 100644 index 0000000..903002d --- /dev/null +++ b/specs/001-build-an-application/contracts/submission_api.md @@ -0,0 +1,219 @@ +# Submission API Contract + +**Scope**: Anonymous feedback submission endpoints (User Story P1) +**Authentication**: None (anonymous access) + +--- + +## GET /submit/{product_slug} + +Display the feedback submission form for a specific product. + +### Request + +**Path Parameters**: +- `product_slug` (string, required): Product's URL-safe identifier + +**Headers**: None required + +**Query Parameters**: None + +### Response + +**Success (200 OK)**: +```html +Content-Type: text/html + + + + Submit Feedback - {Product Name} + +

Submit Feedback for {Product Name}

+
+ + + +
+ + +``` + +**Error (404 Not Found)**: Product does not exist or is archived +```html +Content-Type: text/html + + + + +

Product Not Found

+

The product you're looking for does not exist or is no longer accepting feedback.

+ + +``` + +### Functional Requirements Covered +- FR-001: Public, unauthenticated submission form +- FR-002: Text input up to 10,000 characters +- FR-004: Up to 3 file attachments + +--- + +## POST /submit/{product_slug} + +Submit anonymous feedback for a specific product. + +### Request + +**Path Parameters**: +- `product_slug` (string, required): Product's URL-safe identifier + +**Headers**: +- `Content-Type: multipart/form-data` + +**Form Data**: +- `feedback_text` (string, optional): Feedback text (0-10,000 characters) +- `attachments` (file[], optional): Up to 3 files, max 10MB each + +**Example**: +```http +POST /submit/acme-app HTTP/1.1 +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary + +------WebKitFormBoundary +Content-Disposition: form-data; name="feedback_text" + +The app crashes when uploading files over 5MB. +------WebKitFormBoundary +Content-Disposition: form-data; name="attachments"; filename="screenshot.png" +Content-Type: image/png + +[binary data] +------WebKitFormBoundary-- +``` + +### Response + +**Success (200 OK)**: +```html +Content-Type: text/html + + + + +

Thank You!

+

Your feedback has been submitted successfully.

+

Your feedback ID: {feedback_id}

+ + +``` + +**Error (400 Bad Request)**: Validation failure +```html +Content-Type: text/html + + + + +

Submission Error

+ + + +``` + +**Error (413 Payload Too Large)**: File size exceeds limit +```html +Content-Type: text/html +HTTP/1.1 413 Payload Too Large + + + + +

File Too Large

+

One or more files exceed the 10MB limit.

+ + +``` + +**Error (429 Too Many Requests)**: Rate limit exceeded +```html +Content-Type: text/html +X-RateLimit-Limit: 10 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 1697456789 + + + + +

Too Many Submissions

+

You have exceeded the submission limit of 10 per hour. Please try again later.

+ + +``` + +**Error (451 Unavailable For Legal Reasons)**: Malware detected +```html +Content-Type: text/html + + + + +

Security Error

+

One or more files failed security scanning. Please ensure your files are safe and try again.

+ + +``` + +### Validation Rules + +1. **Text Validation** (FR-002, FR-003): + - Length: 0-10,000 characters + - Encoding: UTF-8, any language accepted + - Empty allowed if attachments present + +2. **Attachment Validation** (FR-004, FR-005, FR-006): + - Count: 0-3 files + - Size: Max 10MB per file + - Types: PDF, DOCX, TXT, JPG, PNG, GIF, WebP + - MIME type validation (server-side) + +3. **Submission Validation** (FR-011, FR-012): + - Must have text OR attachments (not both empty) + +4. **Security** (FR-059, FR-060): + - ClamAV virus scan before storage + - Filename sanitization (remove path traversal) + - MIME type validation + +5. **Rate Limiting** (FR-061): + - 10 submissions per hour per IP address + +### Functional Requirements Covered +- FR-001 to FR-012: Complete submission flow +- FR-055: No IP/session tracking stored +- FR-059, FR-060: File security +- FR-061: Rate limiting + +### Side Effects + +1. **File System**: + - Creates `data/products/{product_id}/feedback/{feedback_id}/` + - Writes `metadata.yaml`, `content.txt` + - Writes `attachments/{filename}` if files uploaded + +2. **Async Processing**: + - Triggers AI analysis background job + - Updates feedback status: `submitted` → `analyzing` + +--- + +## POST /submit/{product_slug}/status/{feedback_id} + +**Note**: This endpoint is OUT OF SCOPE for MVP. Feedback submission is fire-and-forget. Users cannot track submission status anonymously. + +Future consideration: Anonymous status check via feedback ID (requires balancing anonymity with user experience). diff --git a/specs/001-build-an-application/data-model.md b/specs/001-build-an-application/data-model.md new file mode 100644 index 0000000..6dfcafa --- /dev/null +++ b/specs/001-build-an-application/data-model.md @@ -0,0 +1,410 @@ +# Data Model: Anonymous Feedback Platform (Reklamator) + +**Branch**: `001-build-an-application` | **Date**: 2025-10-15 + +This document defines the domain entities, their attributes, relationships, validation rules, and state transitions for the Reklamator application. + +## Entity Overview + +``` +Product (1) ----< (N) Feedback + | | + | |---< (N) Attachment + | | + | |---- (1) AnalysisResult + | + |----< (N) ProductOwner + +Administrator (manages all entities) +``` + +--- + +## Entity Definitions + +### 1. Feedback Submission + +**Description**: Represents a single feedback item submitted by an anonymous user. + +**Storage Location**: `data/products/{product_id}/feedback/{feedback_id}/` + +#### Attributes + +| Field | Type | Required | Validation | Description | +|-------|------|----------|------------|-------------| +| `id` | UUID v4 | Yes | Auto-generated | Unique identifier | +| `product_id` | String | Yes | Must reference existing product | Associated product identifier | +| `original_text` | String | Yes* | 1-10,000 characters | Original feedback text (*empty if file-only submission) | +| `original_language` | String (ISO 639-1) | No | 2-char code | Detected language (e.g., "en", "de", "ja") | +| `submission_timestamp` | ISO 8601 DateTime | Yes | Auto-generated | When feedback was submitted (UTC) | +| `status` | Enum | Yes | See Status enum below | Current processing/review status | +| `category` | Enum | No | See Category enum below | AI-assigned category (null if analysis pending/failed) | +| `attachment_count` | Integer | Yes | 0-3 | Number of attached files | + +#### Status Enum +- `submitted` - Initial state after successful submission +- `analyzing` - AI analysis in progress +- `analysis_failed` - AI analysis encountered error +- `analyzed` - AI analysis completed successfully +- `reviewed` - Product owner has reviewed +- `in_progress` - Product owner marked as being worked on +- `resolved` - Product owner marked as resolved +- `rejected` - Product owner marked as not actionable + +#### Category Enum (AI-assigned) +- `idea` - New concept or suggestion +- `feature_request` - Request for specific functionality +- `bug` - Problem or defect report +- `complaint` - Negative feedback about existing functionality + +#### Validation Rules +- FR-002: `original_text` max length 10,000 characters +- FR-011: At least one of (`original_text`, `attachment_count > 0`) must be true +- FR-012: Cannot be empty (no text AND no attachments) +- FR-027: `submission_timestamp` immutable after creation + +#### State Transitions +``` +submitted → analyzing → analyzed → {reviewed, in_progress, resolved, rejected} + ↓ + analysis_failed (terminal state until manual retry) +``` + +#### File Representation (metadata.yaml) +```yaml +id: "a3f2c1d5-8b4e-4f1a-9c2d-7e6f5a4b3c2d" +product_id: "001-acme-app" +original_language: "en" +submission_timestamp: "2025-10-15T14:32:10Z" +status: "analyzed" +category: "bug" +attachment_count: 2 +attachments: + - filename: "screenshot.png" + size_bytes: 245678 + mime_type: "image/png" + - filename: "error_log.txt" + size_bytes: 1234 + mime_type: "text/plain" +``` + +--- + +### 2. Product/Service + +**Description**: Represents a product or service for which feedback can be collected. + +**Storage Location**: `data/products/{product_id}/config.yaml` + +#### Attributes + +| Field | Type | Required | Validation | Description | +|-------|------|----------|------------|-------------| +| `id` | String | Yes | Unique, URL-safe slug | Product identifier (e.g., "001-acme-app") | +| `name` | String | Yes | 1-100 characters | Display name | +| `description` | String | No | Max 500 characters | Product description | +| `target_language` | String (ISO 639-1) | Yes | 2-char code | Preferred language for AI translations | +| `submission_url_slug` | String | Yes | URL-safe, unique | URL path for submission form (e.g., "/submit/acme-app") | +| `created_date` | ISO 8601 Date | Yes | Auto-generated | When product was registered | +| `status` | Enum | Yes | "active" or "archived" | Current status | +| `assigned_owner_ids` | List[String] | No | Must reference existing users | Product owner user IDs | + +#### Validation Rules +- FR-046: `id` must be unique across all products +- FR-047: `target_language` must be valid ISO 639-1 code +- FR-049: `submission_url_slug` must be unique and URL-safe (alphanumeric + hyphens) +- FR-053: Cannot accept new feedback if `status` is "archived" + +#### State Transitions +``` +active ⇄ archived (bidirectional, admin only) +``` + +#### File Representation (config.yaml) +```yaml +id: "001-acme-app" +name: "Acme Mobile App" +description: "Customer feedback for Acme's flagship mobile application" +target_language: "en" +submission_url_slug: "acme-app" +created_date: "2025-10-01" +status: "active" +assigned_owner_ids: + - "owner-001" + - "owner-002" +statistics: + total_feedback_count: 127 + last_submission: "2025-10-15T14:32:10Z" +``` + +--- + +### 3. Analysis Result + +**Description**: Represents the AI-generated analysis of a feedback submission. + +**Storage Location**: `data/products/{product_id}/feedback/{feedback_id}/analysis.md` +**Additional Storage**: Original text stored in `content.txt` for reference + +#### Attributes (Markdown Format) + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `translated_text` | Markdown | Yes* | Feedback translated to target language (*if different from original) | +| `summary` | Markdown | Yes | Concise 2-3 sentence summary in target language | +| `detected_category` | Enum | Yes | Category assigned by AI (idea/feature_request/bug/complaint) | +| `confidence_score` | Float (0.0-1.0) | Yes | AI confidence in categorization | +| `analysis_timestamp` | ISO 8601 DateTime | Yes | When analysis completed | +| `model_used` | String | Yes | AI model identifier (e.g., "claude-3-haiku-20240307") | +| `error_message` | String | No | Error details if analysis failed | + +#### Validation Rules +- FR-014: `summary` should be 2-3 sentences maximum +- FR-015: `translated_text` required unless original language = target language +- FR-016: Original text preserved in `content.txt` alongside analysis +- FR-017: Detected language stored in feedback metadata.yaml `original_language` + +#### File Representation (analysis.md) +```markdown +# Feedback Analysis + +**Analyzed**: 2025-10-15T14:35:22Z +**Model**: claude-3-haiku-20240307 +**Category**: bug (confidence: 0.92) +**Original Language**: de → **Target Language**: en + +## Summary + +User reports that the app crashes when uploading large files. This appears to be a bug affecting the file upload module, preventing users from submitting documents over 5MB. + +## Translation + +**Original (German):** +> Die App stürzt ab, wenn ich versuche, große Dateien hochzuladen. Jedes Mal wenn ich eine PDF über 5MB hochlade, friert die App ein und schließt sich. + +**Translated (English):** +The app crashes when I try to upload large files. Every time I upload a PDF over 5MB, the app freezes and closes. + +## Attachments + +- screenshot.png (245 KB) +- error_log.txt (1 KB) +``` + +--- + +### 4. Attachment + +**Description**: Represents a file (document or image) uploaded with feedback. + +**Storage Location**: `data/products/{product_id}/feedback/{feedback_id}/attachments/{filename}` + +#### Attributes (stored in feedback metadata.yaml) + +| Field | Type | Required | Validation | Description | +|-------|------|----------|------------|-------------| +| `filename` | String | Yes | Sanitized, max 255 chars | Original filename (sanitized for safety) | +| `size_bytes` | Integer | Yes | Max 10,485,760 (10MB) | File size in bytes | +| `mime_type` | String | Yes | See allowed types | Validated MIME type | +| `upload_timestamp` | ISO 8601 DateTime | Yes | Auto-generated | When file was uploaded | +| `virus_scan_status` | Enum | Yes | "clean" or "infected" | ClamAV scan result | + +#### Allowed MIME Types +- Documents: `application/pdf`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (DOCX), `text/plain` +- Images: `image/jpeg`, `image/png`, `image/gif`, `image/webp` + +#### Validation Rules +- FR-004: Maximum 3 attachments per feedback +- FR-006: Maximum 10MB per file +- FR-026: Preserve original filename (sanitized) +- FR-059: Sanitize filename to prevent directory traversal +- FR-060: Must pass ClamAV virus scan before storage + +#### Security Sanitization +- Remove directory traversal patterns: `../`, `..\\`, absolute paths +- Replace unsafe characters: `<>:"|?*` +- Limit filename length to 255 characters +- If duplicate filename, append counter: `file.pdf` → `file_2.pdf` + +--- + +### 5. Product Owner + +**Description**: Represents an authenticated user responsible for reviewing feedback for one or more products. + +**Storage Location**: `data/users.yaml` + +#### Attributes + +| Field | Type | Required | Validation | Description | +|-------|------|----------|------------|-------------| +| `id` | String | Yes | Unique | User identifier (e.g., "owner-001") | +| `email` | String | Yes | Valid email, unique | Login email address | +| `password_hash` | String (bcrypt) | Yes | bcrypt format | Hashed password (never store plaintext) | +| `name` | String | Yes | 1-100 characters | Display name | +| `role` | Enum | Yes | "product_owner" | User role (always "product_owner" for this entity) | +| `assigned_product_ids` | List[String] | Yes | Must reference existing products | Products this owner can access | +| `created_date` | ISO 8601 Date | Yes | Auto-generated | Account creation date | +| `last_login` | ISO 8601 DateTime | No | Auto-updated | Last successful login | + +#### Validation Rules +- FR-048: Can be assigned to multiple products +- FR-056: Must authenticate to access dashboard +- FR-063: Passwords hashed with bcrypt (cost factor 12) +- FR-033: Can only view feedback for `assigned_product_ids` + +#### File Representation (users.yaml entry) +```yaml +users: + - id: "owner-001" + email: "jane.smith@example.com" + password_hash: "$2b$12$KIXxBt5H4vE2zT9vN8FqOe9Jx..." + name: "Jane Smith" + role: "product_owner" + assigned_product_ids: + - "001-acme-app" + - "002-beta-service" + created_date: "2025-09-15" + last_login: "2025-10-15T09:23:11Z" +``` + +--- + +### 6. Administrator + +**Description**: Represents a privileged user who can register products, assign owners, and manage system configuration. + +**Storage Location**: `data/users.yaml` (same file as Product Owners) + +#### Attributes + +| Field | Type | Required | Validation | Description | +|-------|------|----------|------------|-------------| +| `id` | String | Yes | Unique | User identifier (e.g., "admin-001") | +| `email` | String | Yes | Valid email, unique | Login email address | +| `password_hash` | String (bcrypt) | Yes | bcrypt format | Hashed password | +| `name` | String | Yes | 1-100 characters | Display name | +| `role` | Enum | Yes | "admin" | User role (always "admin" for this entity) | +| `assigned_product_ids` | List[String] | Yes | Empty list | Empty = access to all products | +| `created_date` | ISO 8601 Date | Yes | Auto-generated | Account creation date | +| `last_login` | ISO 8601 DateTime | No | Auto-updated | Last successful login | + +#### Validation Rules +- Admin role grants full access regardless of `assigned_product_ids` +- FR-045: Can create/modify/archive products +- FR-048: Can assign/unassign product owners +- Same authentication requirements as Product Owner (FR-056, FR-063) + +#### File Representation (users.yaml entry) +```yaml +users: + - id: "admin-001" + email: "admin@reklamator.local" + password_hash: "$2b$12$vL3Fx9..." + name: "System Administrator" + role: "admin" + assigned_product_ids: [] # Empty = all access + created_date: "2025-09-01" + last_login: "2025-10-15T10:45:33Z" +``` + +--- + +## Domain Rules & Invariants + +### Cross-Entity Rules + +1. **Product-Feedback Relationship** (1:N) + - Every Feedback must reference exactly one valid Product + - Product can have zero or many Feedback items + - Archived products cannot receive new feedback (FR-053) + +2. **Feedback-Attachment Relationship** (1:N) + - Feedback can have 0-3 Attachments (FR-004, FR-010) + - Attachments cannot exist without parent Feedback (cascade delete) + +3. **Feedback-AnalysisResult Relationship** (1:1) + - Every analyzed Feedback has exactly one AnalysisResult + - AnalysisResult created asynchronously after Feedback submission + - Original content preserved even if analysis fails (FR-020) + +4. **Product-Owner Relationship** (N:M) + - Product can have 1 or more assigned Product Owners (FR-048) + - Product Owner can be assigned to multiple Products + - Admin users bypass assignment logic (implicit access to all) + +5. **Anonymity Constraint** (Global) + - No IP addresses stored in Feedback metadata (FR-055, SC-010) + - No session tracking for anonymous submissions + - Rate limiting uses IP for abuse prevention only (not persisted) + +### Deletion Rules + +- **Feedback Deletion**: Deletes metadata.yaml, content.txt, analysis.md, and all attachments/ +- **Product Archival**: Sets status to "archived", preserves all feedback (FR-052) +- **Product Owner Removal**: Unassigns from products, does not delete feedback +- **Cascade Protection**: Cannot delete Product with active feedback (archive instead) + +--- + +## Storage Implementation Notes + +### Directory Structure Example + +``` +data/ +├── users.yaml # All users (admins + owners) +└── products/ + ├── 001-acme-app/ + │ ├── config.yaml # Product metadata + │ └── feedback/ + │ ├── a3f2c1d5-8b4e-.../ + │ │ ├── metadata.yaml # Feedback + attachment metadata + │ │ ├── content.txt # Original feedback text + │ │ ├── analysis.md # AI analysis report + │ │ └── attachments/ + │ │ ├── screenshot.png + │ │ └── error_log.txt + │ └── b7e1f3d2-4a9c-.../ + │ ├── metadata.yaml + │ ├── content.txt + │ └── analysis.md # No attachments/ for this one + └── 002-beta-service/ + ├── config.yaml + └── feedback/ + └── ... +``` + +### File Format Standards + +- **YAML**: UTF-8 encoding, 2-space indentation, explicit type declarations +- **Markdown**: CommonMark specification, UTF-8 encoding +- **Text Files**: UTF-8 encoding with BOM handling + +### Indexing Strategy (Performance) + +For dashboard performance (SC-008, SC-014): +- Cache product feedback counts in `config.yaml` statistics +- Implement pagination (50 items per page) +- Load metadata.yaml only, defer analysis.md loading until detail view +- File modification times used for sorting (newest first per FR-041) + +--- + +## Validation Summary by Functional Requirement + +| FR | Validation Location | Rule | +|----|---------------------|------| +| FR-002 | Feedback.original_text | Max 10,000 characters | +| FR-004 | Feedback.attachment_count | 0-3 attachments | +| FR-006 | Attachment.size_bytes | Max 10MB per file | +| FR-011 | Feedback validation | At least text OR files required | +| FR-012 | Feedback validation | Cannot be completely empty | +| FR-046 | Product.id | Must be unique | +| FR-048 | Product.assigned_owner_ids | 1+ owners required | +| FR-053 | Product status check | Reject if archived | +| FR-063 | User.password_hash | bcrypt with cost 12 | + +--- + +**Next Steps**: Define API contracts in `/contracts/` directory diff --git a/specs/001-build-an-application/plan.md b/specs/001-build-an-application/plan.md new file mode 100644 index 0000000..74ab63e --- /dev/null +++ b/specs/001-build-an-application/plan.md @@ -0,0 +1,188 @@ +# Implementation Plan: Anonymous Feedback Platform (Reklamator) + +**Branch**: `001-build-an-application` | **Date**: 2025-10-15 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/001-build-an-application/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. + +## Summary + +Build a minimal web application using Flask that enables anonymous feedback submission with AI-powered analysis and translation. The system uses a file-based storage approach with folders for each submission, YAML metadata files, and markdown-formatted AI analysis reports. Product owners access analyzed feedback through an authenticated web dashboard. Design prioritizes simplicity and functionality over aesthetics - plain HTML without CSS frameworks or JavaScript libraries. + +## Technical Context + +**Language/Version**: Python 3.11+ +**Primary Dependencies**: Flask (web framework), no CSS frameworks, no JavaScript libraries +**Storage**: File-based - folders per feedback item with YAML metadata and markdown reports +**Testing**: pytest (contract and integration tests prioritized per constitution) +**Target Platform**: Linux server (web application) +**Project Type**: web (backend + frontend, but minimal frontend without frameworks) +**AI Integration**: NEEDS CLARIFICATION - Claude API or pluggable AI provider interface +**Performance Goals**: Handle 100 concurrent submissions, <3s dashboard load for 1000 items +**Constraints**: <30s AI analysis time for 95% of submissions, complete anonymity (no IP/session tracking) +**Scale/Scope**: MVP supports 100 products, 10,000 feedback items per product, 50+ languages +**File Upload**: NEEDS CLARIFICATION - malware scanning approach, storage location strategy +**Authentication**: NEEDS CLARIFICATION - session management approach for product owners +**Rate Limiting**: NEEDS CLARIFICATION - implementation strategy for submission abuse prevention + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +### ✅ I. Specification-First Development +**Status**: PASS +Complete specification exists at `specs/001-build-an-application/spec.md` with prioritized user stories (P1-P4), 64 functional requirements with unique IDs (FR-001 to FR-064), measurable success criteria (SC-001 to SC-014), and comprehensive edge cases. All user stories are independently testable. + +### ✅ II. Test-First Discipline +**Status**: PASS (Will be enforced during implementation) +Plan includes pytest as testing framework. Implementation phase will follow mandatory workflow: write tests → verify failures → implement code → refactor. Contract and integration tests prioritized per constitution. + +### ✅ III. Independent User Stories +**Status**: PASS +Four user stories explicitly prioritized (P1: Anonymous Submission, P2: AI Analysis, P3: Dashboard, P4: Product Management). Each story is independently deliverable and testable. P1 can function standalone, P2 depends only on P1, P3 on P1+P2, P4 adds multi-product support. + +### ✅ IV. Simplicity & Justification +**Status**: PASS +Design explicitly minimizes complexity: plain HTML without CSS frameworks, no JavaScript libraries, file-based storage (no database), Flask for web framework (minimal dependencies). User input emphasizes "designed as simple as possible" and "functionality over design." + +**Potential Complexity Point**: File-based storage vs. database +- **Decision**: File-based storage with folder-per-feedback structure +- **Rationale**: Simpler deployment, no database setup/maintenance, natural fit for storing files+metadata together, sufficient for MVP scale (100 products × 10k items) +- **Alternative Rejected**: PostgreSQL/SQLite - adds operational complexity, requires schema migrations, doesn't simplify file attachment handling + +### ✅ V. Documentation as Code +**Status**: PASS +Specification-driven workflow with all documentation in version control under `/specs/001-build-an-application/`. This plan will generate: research.md, data-model.md, contracts/, quickstart.md per constitution requirements. + +### Gate Result: ✅ PASS - Proceed to Phase 0 Research + +No constitutional violations detected. Complexity Tracking table remains empty. + +## Project Structure + +### Documentation (this feature) + +``` +specs/[###-feature]/ +├── plan.md # This file (/speckit.plan command output) +├── research.md # Phase 0 output (/speckit.plan command) +├── data-model.md # Phase 1 output (/speckit.plan command) +├── quickstart.md # Phase 1 output (/speckit.plan command) +├── contracts/ # Phase 1 output (/speckit.plan command) +└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) +``` + +### Source Code (repository root) + +``` +reklamator/ +├── app/ +│ ├── __init__.py # Flask app factory +│ ├── routes/ +│ │ ├── __init__.py +│ │ ├── submission.py # Anonymous feedback submission endpoints +│ │ ├── dashboard.py # Product owner dashboard endpoints +│ │ └── admin.py # Product/owner management endpoints +│ ├── services/ +│ │ ├── __init__.py +│ │ ├── feedback_storage.py # File-based storage operations +│ │ ├── ai_analyzer.py # AI analysis/translation interface +│ │ └── auth.py # Session management +│ ├── models/ +│ │ ├── __init__.py +│ │ ├── feedback.py # Feedback domain model +│ │ ├── product.py # Product domain model +│ │ └── user.py # Product owner/admin model +│ ├── templates/ # Plain HTML templates (Jinja2) +│ │ ├── submission_form.html +│ │ ├── dashboard.html +│ │ ├── feedback_detail.html +│ │ └── admin_products.html +│ └── utils/ +│ ├── __init__.py +│ ├── file_validator.py # File upload validation +│ └── rate_limiter.py # Submission rate limiting +│ +├── data/ # File-based storage root +│ └── products/ +│ └── {product-id}/ +│ └── feedback/ +│ └── {feedback-id}/ +│ ├── metadata.yaml +│ ├── analysis.md +│ └── attachments/ +│ +├── tests/ +│ ├── contract/ # API contract tests +│ │ ├── test_submission_api.py +│ │ ├── test_dashboard_api.py +│ │ └── test_admin_api.py +│ ├── integration/ # User journey tests +│ │ ├── test_feedback_submission_flow.py +│ │ ├── test_ai_analysis_flow.py +│ │ └── test_dashboard_access_flow.py +│ └── unit/ # Optional unit tests for complex logic +│ ├── test_feedback_storage.py +│ └── test_file_validator.py +│ +├── config/ +│ ├── development.py +│ ├── production.py +│ └── testing.py +│ +├── requirements.txt +├── pytest.ini +└── run.py # Application entry point +``` + +**Structure Decision**: Selected web application structure (Option 2 variant) with backend-focused layout since frontend is minimal (plain HTML templates). Flask follows a single-project structure but organized by layers (routes/services/models). The `data/` directory implements the file-based storage requirement with nested folders per product and feedback item. Templates directory contains plain HTML served by Flask without separate frontend build process. + +## Complexity Tracking + +*Fill ONLY if Constitution Check has violations that must be justified* + +No violations detected. Table remains empty. + +--- + +## Post-Design Constitution Re-Check + +*Re-evaluated after Phase 1 design completion* + +### ✅ I. Specification-First Development +**Status**: PASS (unchanged) +Design artifacts (research.md, data-model.md, contracts/, quickstart.md) generated from specification. No implementation code written yet. + +### ✅ II. Test-First Discipline +**Status**: PASS (unchanged) +API contracts define testable behaviors. Contract tests can be written before implementation. Quickstart guide includes test-first workflow examples. + +### ✅ III. Independent User Stories +**Status**: PASS (unchanged) +Data model and API contracts support independent implementation of P1→P2→P3→P4 stories. Each has clear endpoints and data structures. + +### ✅ IV. Simplicity & Justification +**Status**: PASS (confirmed post-design) +- File-based storage design confirmed (YAML + Markdown) +- No database complexity introduced +- Minimal dependencies: Flask + 7 small extensions +- Plain HTML templates (no CSS frameworks, no JavaScript) +- Direct file I/O (no ORM or abstraction layers) +- Single-project structure (no microservices) + +**Design Review**: All research decisions favor simplicity. No new complexity introduced during Phase 1. + +### ✅ V. Documentation as Code +**Status**: PASS (enhanced) +Generated artifacts: +- ✅ research.md (7 decision records) +- ✅ data-model.md (6 entities fully specified) +- ✅ contracts/ (3 API contract documents: submission, dashboard, admin) +- ✅ quickstart.md (developer onboarding guide) +- ✅ CLAUDE.md (agent context updated) + +All documentation version-controlled, linked to spec.md. + +### Final Gate Result: ✅ PASS - Ready for Phase 2 (Task Generation) + +No constitutional violations introduced during design phase. Proceed to `/speckit.tasks` command. diff --git a/specs/001-build-an-application/quickstart.md b/specs/001-build-an-application/quickstart.md new file mode 100644 index 0000000..0f68f7b --- /dev/null +++ b/specs/001-build-an-application/quickstart.md @@ -0,0 +1,469 @@ +# Quickstart Guide: Reklamator Development + +**Branch**: `001-build-an-application` | **Date**: 2025-10-15 + +This guide helps developers set up the Reklamator development environment and understand the project structure. + +--- + +## Prerequisites + +- Python 3.11 or higher +- ClamAV daemon (`clamd`) for malware scanning +- Git +- Virtual environment tool (venv) + +--- + +## Initial Setup + +### 1. Clone Repository + +```bash +git clone +cd reklamator +git checkout 001-build-an-application +``` + +### 2. Create Virtual Environment + +```bash +python3 -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` + +### 3. Install Dependencies + +```bash +pip install -r requirements.txt +``` + +**Expected Core Dependencies**: +- Flask 3.0+ +- Flask-Login (session management) +- Flask-Limiter (rate limiting) +- Flask-WTF (CSRF protection) +- anthropic (Claude API client) +- clamd (ClamAV integration) +- bcrypt (password hashing) +- PyYAML (configuration files) +- pytest, pytest-flask (testing) + +### 4. Install and Configure ClamAV + +**Ubuntu/Debian**: +```bash +sudo apt-get update +sudo apt-get install clamav clamav-daemon +sudo systemctl start clamav-daemon +sudo systemctl enable clamav-daemon +``` + +**macOS**: +```bash +brew install clamav +brew services start clamav +``` + +**Verify ClamAV is running**: +```bash +clamdscan --version +``` + +### 5. Set Up Environment Variables + +Create `.env` file in project root: + +```bash +# Flask Configuration +FLASK_APP=run.py +FLASK_ENV=development +SECRET_KEY=your-secret-key-here-change-in-production + +# Claude API +ANTHROPIC_API_KEY=your-claude-api-key-here + +# ClamAV +CLAMD_SOCKET=/var/run/clamav/clamd.ctl # Adjust path for your system + +# File Storage +DATA_DIR=./data + +# Rate Limiting +RATE_LIMIT_ENABLED=true +RATE_LIMIT_PER_HOUR=10 +``` + +**Get Claude API Key**: +1. Sign up at https://console.anthropic.com/ +2. Create an API key +3. Add to `.env` file + +### 6. Initialize Data Directory + +```bash +mkdir -p data/products +``` + +### 7. Create Initial Admin User + +Create `data/users.yaml`: + +```yaml +users: + - id: "admin-001" + email: "admin@localhost" + password_hash: "$2b$12$KIXxBt5H4vE2zT9vN8FqOe9JxwLxPqz0q5kYv2Z3j4RQvN8FqOe9J" # Password: "admin123" + name: "Admin User" + role: "admin" + assigned_product_ids: [] + created_date: "2025-10-15" + last_login: null +``` + +**Security Note**: Change the password immediately after first login! + +To generate a new password hash: +```python +import bcrypt +password = "your-password-here" +hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12)) +print(hash.decode('utf-8')) +``` + +--- + +## Running the Application + +### Development Server + +```bash +python run.py +``` + +Application will be available at: http://localhost:5000 + +### Production Server (Gunicorn) + +```bash +gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()" +``` + +--- + +## Project Structure Overview + +``` +reklamator/ +├── app/ # Application code +│ ├── __init__.py # Flask app factory +│ ├── routes/ # HTTP endpoints +│ │ ├── submission.py # Anonymous feedback submission +│ │ ├── dashboard.py # Product owner dashboard +│ │ └── admin.py # Admin interface +│ ├── services/ # Business logic +│ │ ├── feedback_storage.py # File-based storage operations +│ │ ├── ai_analyzer.py # AI analysis/translation +│ │ └── auth.py # Authentication +│ ├── models/ # Domain models +│ │ ├── feedback.py # Feedback entity +│ │ ├── product.py # Product entity +│ │ └── user.py # User entity +│ ├── templates/ # HTML templates (Jinja2) +│ └── utils/ # Utilities +│ ├── file_validator.py # File upload validation +│ └── rate_limiter.py # Rate limiting +│ +├── data/ # File-based storage +│ ├── users.yaml # User accounts +│ └── products/ # Product-specific data +│ └── {product-id}/ +│ ├── config.yaml # Product metadata +│ └── feedback/ +│ └── {feedback-id}/ +│ ├── metadata.yaml +│ ├── content.txt +│ ├── analysis.md +│ └── attachments/ +│ +├── tests/ # Test suite +│ ├── contract/ # API contract tests +│ ├── integration/ # User journey tests +│ └── unit/ # Unit tests +│ +├── config/ # Configuration files +│ ├── development.py +│ ├── production.py +│ └── testing.py +│ +├── specs/ # Feature specifications (this directory) +├── requirements.txt +├── pytest.ini +├── .env # Environment variables (not in git) +└── run.py # Application entry point +``` + +--- + +## Common Development Tasks + +### Creating a Test Product + +1. Log in as admin: http://localhost:5000/login + - Email: `admin@localhost` + - Password: `admin123` + +2. Navigate to: http://localhost:5000/admin/products + +3. Click "Create New Product" and fill in: + - ID: `001-test-product` + - Name: `Test Product` + - Target Language: `en` + - Submission URL Slug: `test-product` + - Assign yourself as product owner + +4. Access submission form: http://localhost:5000/submit/test-product + +### Submitting Test Feedback + +1. Visit: http://localhost:5000/submit/test-product +2. Enter feedback text +3. Optionally attach files (max 3, max 10MB each) +4. Submit + +Feedback will be processed asynchronously. Check the dashboard to view analysis results. + +### Viewing Feedback in Dashboard + +1. Log in: http://localhost:5000/login +2. Dashboard: http://localhost:5000/dashboard +3. Click on feedback item to view details + +### Running Tests + +**All tests**: +```bash +pytest +``` + +**Contract tests only**: +```bash +pytest tests/contract/ +``` + +**Integration tests only**: +```bash +pytest tests/integration/ +``` + +**With coverage**: +```bash +pytest --cov=app --cov-report=html +``` + +**Test-first workflow** (per constitution): +1. Write test for new feature (should fail) +2. Run test to verify failure +3. Implement feature +4. Run test to verify success +5. Refactor if needed + +--- + +## API Endpoints Reference + +### Anonymous Submission +- `GET /submit/{product_slug}` - Submission form +- `POST /submit/{product_slug}` - Submit feedback + +### Authentication +- `GET /login` - Login form +- `POST /login` - Authenticate +- `GET /logout` - Log out + +### Dashboard (Product Owners) +- `GET /dashboard` - Feedback list (with filters) +- `GET /feedback/{feedback_id}` - Feedback detail +- `POST /feedback/{feedback_id}/status` - Update status +- `GET /feedback/{feedback_id}/attachment/{filename}` - Download attachment + +### Admin +- `GET /admin/products` - List products +- `GET /admin/products/new` - Create product form +- `POST /admin/products` - Create product +- `GET /admin/products/{id}/edit` - Edit product form +- `POST /admin/products/{id}` - Update product +- `POST /admin/products/{id}/archive` - Archive product +- `GET /admin/users` - List users +- `POST /admin/users` - Create user +- `POST /admin/users/{id}` - Update user + +Full API contracts: See `/specs/001-build-an-application/contracts/` + +--- + +## Configuration + +### Development Configuration (`config/development.py`) + +```python +DEBUG = True +TESTING = False +SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key') +DATA_DIR = os.environ.get('DATA_DIR', './data') +ANTHROPIC_API_KEY = os.environ.get('ANTHROPIC_API_KEY') +CLAMD_SOCKET = os.environ.get('CLAMD_SOCKET', '/var/run/clamav/clamd.ctl') +MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10MB max upload +RATE_LIMIT_ENABLED = True +RATE_LIMIT_PER_HOUR = 10 +``` + +### Production Configuration (`config/production.py`) + +```python +DEBUG = False +TESTING = False +SECRET_KEY = os.environ.get('SECRET_KEY') # Required, no default +DATA_DIR = os.environ.get('DATA_DIR', '/var/lib/reklamator/data') +ANTHROPIC_API_KEY = os.environ.get('ANTHROPIC_API_KEY') # Required +CLAMD_SOCKET = os.environ.get('CLAMD_SOCKET', '/var/run/clamav/clamd.ctl') +MAX_CONTENT_LENGTH = 10 * 1024 * 1024 +RATE_LIMIT_ENABLED = True +RATE_LIMIT_PER_HOUR = 10 +SESSION_COOKIE_SECURE = True # HTTPS only +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SAMESITE = 'Lax' +``` + +--- + +## Troubleshooting + +### ClamAV Connection Error + +**Error**: `pyclamd.ConnectionError: Could not connect to clamd` + +**Solution**: +1. Verify ClamAV is running: `sudo systemctl status clamav-daemon` +2. Check socket path: `ls /var/run/clamav/clamd.ctl` +3. Update `CLAMD_SOCKET` in `.env` if needed +4. Restart ClamAV: `sudo systemctl restart clamav-daemon` + +### Claude API Error + +**Error**: `anthropic.APIError: Invalid API key` + +**Solution**: +1. Verify API key in `.env` file +2. Check key is active at https://console.anthropic.com/ +3. Ensure no extra whitespace in key + +### File Upload Fails + +**Error**: `413 Payload Too Large` + +**Solution**: +- Check file size (max 10MB per file) +- Check total payload size (3 files + form data) +- Verify `MAX_CONTENT_LENGTH` in config + +**Error**: `Unsupported file type` + +**Solution**: +- Verify file extension: `.pdf`, `.docx`, `.txt`, `.jpg`, `.png`, `.gif`, `.webp` +- Check MIME type matches extension + +### Rate Limit Exceeded + +**Error**: `429 Too Many Requests` + +**Solution**: +- Wait 1 hour before retrying +- For development, disable rate limiting: `RATE_LIMIT_ENABLED=false` in `.env` +- Or increase limit: `RATE_LIMIT_PER_HOUR=100` + +--- + +## Development Guidelines + +### Test-First Discipline (Constitutional Requirement) + +1. **Before implementing any feature**: + - Write contract/integration test + - Run test to verify it fails + - Implement feature + - Run test to verify success + +2. **Test organization**: + - Contract tests: Test API endpoints (HTTP requests/responses) + - Integration tests: Test user journeys (multi-step workflows) + - Unit tests: Test complex business logic in isolation + +3. **Example test-first workflow**: + +```python +# Step 1: Write test (tests/contract/test_submission_api.py) +def test_submit_feedback_with_text_only(client): + response = client.post('/submit/test-product', data={ + 'feedback_text': 'This is test feedback' + }) + assert response.status_code == 200 + assert b'Thank You!' in response.data + +# Step 2: Run test (should FAIL - endpoint not implemented) +# pytest tests/contract/test_submission_api.py::test_submit_feedback_with_text_only + +# Step 3: Implement feature (app/routes/submission.py) +@bp.route('/submit/', methods=['POST']) +def submit_feedback(product_slug): + # Implementation here + pass + +# Step 4: Run test again (should PASS) +# pytest tests/contract/test_submission_api.py::test_submit_feedback_with_text_only +``` + +### Code Style + +- Follow PEP 8 +- Use type hints where helpful +- Keep functions small and focused +- Prefer clear names over comments +- Run linting: `flake8 app/` +- Run formatting: `black app/` + +### Git Workflow + +- Feature branch: `001-build-an-application` (already created) +- Commit messages: Descriptive, imperative mood +- Test before committing +- Regular integration to main branch + +--- + +## Next Steps + +1. **Set up environment** following steps above +2. **Run tests** to verify setup: `pytest` +3. **Start development server**: `python run.py` +4. **Create test product** via admin interface +5. **Submit test feedback** via submission form +6. **Review implementation plan**: `/specs/001-build-an-application/plan.md` +7. **Begin task implementation**: Wait for `/specs/001-build-an-application/tasks.md` (generated by `/speckit.tasks`) + +--- + +## Resources + +- **Feature Specification**: `/specs/001-build-an-application/spec.md` +- **Implementation Plan**: `/specs/001-build-an-application/plan.md` +- **Research**: `/specs/001-build-an-application/research.md` +- **Data Model**: `/specs/001-build-an-application/data-model.md` +- **API Contracts**: `/specs/001-build-an-application/contracts/` +- **Flask Documentation**: https://flask.palletsprojects.com/ +- **Claude API Documentation**: https://docs.anthropic.com/ +- **Pytest Documentation**: https://docs.pytest.org/ + +--- + +**Questions?** Refer to the specification documents or implementation plan for detailed requirements and design decisions. diff --git a/specs/001-build-an-application/research.md b/specs/001-build-an-application/research.md new file mode 100644 index 0000000..5c4559b --- /dev/null +++ b/specs/001-build-an-application/research.md @@ -0,0 +1,366 @@ +# Research: Anonymous Feedback Platform (Reklamator) + +**Branch**: `001-build-an-application` | **Date**: 2025-10-15 + +This document resolves all NEEDS CLARIFICATION items identified in the Technical Context section of plan.md. + +## 1. AI Integration Approach + +### Decision: Pluggable AI provider interface with Claude as default + +**Rationale**: +- User input specifies "we use claude but it can be any other service as well" +- Designing for extensibility aligns with good architectural practice +- Enables future migration to different AI providers without major refactoring + +**Implementation Approach**: +- Abstract base class `AIAnalyzer` defining interface: `analyze_feedback(text: str, target_lang: str) -> AnalysisResult` +- Concrete implementation `ClaudeAnalyzer` using Anthropic API +- Configuration-driven provider selection +- API key management via environment variables + +**Alternatives Considered**: +- **Hard-coded Claude API integration**: Simpler initially but violates user requirement for provider flexibility +- **LangChain framework**: Adds significant dependency weight for simple translation/categorization task +- **Multiple provider implementations from start**: Premature complexity - implement Claude first, abstract as needed + +**Claude API Specifics**: +- Use `anthropic` Python SDK +- Model: `claude-3-haiku-20240307` for cost-effective analysis (fast, sufficient for categorization/translation) +- Prompt design: Single API call with structured output for category, summary, translation +- Error handling: Retry logic for transient failures, graceful degradation for persistent errors + +**Research References**: +- Anthropic API Documentation: https://docs.anthropic.com/ +- Python SDK: https://github.com/anthropics/anthropic-sdk-python + +--- + +## 2. File Upload - Malware Scanning Approach + +### Decision: ClamAV integration via clamd for virus scanning + +**Rationale**: +- FR-060 requires malware scanning before storage +- ClamAV is open-source, widely used, actively maintained +- `clamd` provides Python bindings for integration +- Suitable for on-premise deployment matching file-based storage philosophy + +**Implementation Approach**: +- Install ClamAV daemon (`clamd`) as system service +- Use `clamd` Python library for scanning uploaded files +- Scan files synchronously during upload before writing to disk +- Reject files that fail virus scan with clear error message +- Log scanning failures for security monitoring + +**Configuration**: +- Maximum file size: 10MB per file (FR-006) +- Allowed extensions: `.pdf`, `.docx`, `.txt`, `.jpg`, `.png`, `.gif`, `.webp` +- MIME type validation in addition to extension checking +- Temporary upload storage cleaned after scan (pass or fail) + +**Alternatives Considered**: +- **Cloud-based scanning (VirusTotal API)**: Violates anonymity requirement (uploads data externally), adds latency +- **No scanning**: Violates FR-060 security requirement +- **Manual review**: Not scalable, delays feedback processing +- **Python-based scanning (yara-python)**: More complex to configure, less comprehensive than ClamAV + +**Dependencies**: +- `clamd` Python library +- ClamAV daemon installed on server + +--- + +## 3. File Upload - Storage Location Strategy + +### Decision: Local filesystem storage in `data/products/{product-id}/feedback/{feedback-id}/attachments/` + +**Rationale**: +- Aligns with file-based storage architecture (no database) +- User input specifies "folders foreach user input" and "files in a folder" +- Simple to implement, backup, and inspect +- No additional service dependencies +- Sufficient for MVP scale (100 products × 10k items × 3 files × 10MB = ~30TB worst case) + +**Directory Structure**: +``` +data/ +└── products/ + └── {product-id}/ # e.g., "001-acme-app" + ├── config.yaml # Product metadata (name, target language, owners) + └── feedback/ + └── {feedback-id}/ # UUID v4, e.g., "a3f2c1d5-..." + ├── metadata.yaml # Feedback metadata (timestamp, status, category, etc.) + ├── content.txt # Original feedback text + ├── analysis.md # AI-generated analysis report + └── attachments/ + ├── original_filename_1.pdf + ├── original_filename_2.png + └── original_filename_3.jpg +``` + +**File Naming**: +- Preserve original filenames to maintain user context +- Sanitize filenames to prevent directory traversal (strip `../`, absolute paths, etc.) +- Handle duplicate filenames by appending counter if needed + +**Alternatives Considered**: +- **Cloud storage (S3/GCS)**: Adds external dependency, cost, complexity; overkill for MVP +- **Flat directory per product**: Poor scalability, difficult to organize metadata +- **Database with BLOB storage**: Contradicts file-based storage decision, adds DB complexity +- **Content-addressed storage (hash-based filenames)**: Loses original filename context, complicates presentation + +**Backup Strategy** (out of scope for MVP but noted): +- Simple filesystem backup via rsync/tar sufficient +- Can upgrade to cloud sync if needed later + +--- + +## 4. Authentication - Session Management Approach + +### Decision: Flask-Login with server-side sessions for product owners/admins + +**Rationale**: +- Flask-Login is standard, well-tested session management for Flask +- Server-side sessions prevent token tampering +- Simple username/password authentication sufficient for MVP +- FR-056, FR-063 require authentication and secure password storage + +**Implementation Approach**: +- Use `Flask-Login` extension for session management +- Store user credentials in simple YAML file (products/users.yaml) for MVP consistency with file-based approach +- Hash passwords with `bcrypt` (FR-063) +- Session cookies: `HttpOnly`, `Secure` (HTTPS only), `SameSite=Lax` +- Session timeout: 24 hours of inactivity + +**User Model**: +```yaml +users: + - id: "admin-001" + email: "admin@example.com" + password_hash: "$2b$12$..." + role: "admin" + assigned_products: [] # Empty = all products access + + - id: "owner-001" + email: "owner@example.com" + password_hash: "$2b$12$..." + role: "product_owner" + assigned_products: ["001-acme-app", "002-beta-service"] +``` + +**Access Control**: +- Admins: Full access to all products, can manage products/owners +- Product Owners: Read-only access to assigned products only (FR-033) +- Anonymous users: Submission form access only (no authentication) + +**Alternatives Considered**: +- **JWT tokens**: More complex, unnecessary for server-rendered HTML application +- **OAuth/SAML**: Over-engineered for MVP, adds external identity provider dependency +- **Database-backed sessions**: Contradicts file-based architecture +- **No authentication**: Violates FR-056 requirement + +**Dependencies**: +- `Flask-Login` extension +- `bcrypt` for password hashing + +--- + +## 5. Rate Limiting - Implementation Strategy + +### Decision: Flask-Limiter with IP-based rate limiting for submission endpoint + +**Rationale**: +- FR-061 requires rate limiting (suggested: 10 submissions/hour/IP) +- Flask-Limiter is standard, well-maintained Flask extension +- IP-based limiting suitable for anonymous submissions +- In-memory storage sufficient for MVP (single server deployment) + +**Implementation Approach**: +- Use `Flask-Limiter` extension +- Apply rate limit decorator to submission route: `@limiter.limit("10 per hour")` +- Storage backend: In-memory (default) for MVP +- Return HTTP 429 Too Many Requests with clear error message +- Exempt authenticated admin users from rate limits (for testing) + +**Rate Limit Configuration**: +- Anonymous submission: 10 requests per hour per IP address +- Dashboard/admin routes: No rate limiting (authenticated users only) +- Rate limit headers included in response: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` + +**Considerations**: +- IP-based limiting can be circumvented via VPN/proxy but sufficient deterrent for casual abuse +- Behind proxy/load balancer: Configure Flask-Limiter to read `X-Forwarded-For` header +- Note: FR-055 requires no fingerprinting for identification - rate limiting is for abuse prevention only, not user tracking + +**Alternatives Considered**: +- **CAPTCHA (hCaptcha/reCAPTCHA)**: Adds friction to user experience, contradicts "lower barriers" goal +- **Redis-backed rate limiting**: Unnecessary complexity for single-server MVP +- **No rate limiting**: Violates FR-061 requirement, leaves system vulnerable to abuse +- **Token bucket per session**: Requires session tracking for anonymous users, violates anonymity + +**Dependencies**: +- `Flask-Limiter` extension + +--- + +## 6. Flask Best Practices for Simple HTML Applications + +### Decision: Server-side rendering with Jinja2 templates, no JavaScript + +**Rationale**: +- User explicitly specifies "plain html using flask" and "does not use any css frameworks or javascript libraries" +- Server-side rendering eliminates frontend build complexity +- Jinja2 included with Flask, no additional dependencies +- Forms use standard HTTP POST/GET, progressive enhancement approach + +**Template Approach**: +- Minimal inline CSS for basic layout (no framework) +- Semantic HTML5 for accessibility +- Server-side form validation with error display +- Standard browser form controls (no custom widgets) + +**Form Handling**: +- POST requests for submissions +- Server-side validation with error messages +- Flash messages for user feedback +- Redirect-after-POST pattern to prevent duplicate submissions + +**No JavaScript Requirement** (Edge Case from spec.md line 105): +- "What happens when a user's browser doesn't support JavaScript" +- Answer: Application works fully without JavaScript (no JS used) +- File uploads work via standard HTML `` + +**Best Practices Applied**: +- Flask app factory pattern for testability +- Blueprint organization for routes +- Environment-based configuration +- CSRF protection via Flask-WTF (even for simple forms) + +**Dependencies**: +- Flask (includes Jinja2) +- Flask-WTF for CSRF protection + +--- + +## 7. Python Dependency Management + +### Decision: requirements.txt with pinned versions for reproducibility + +**Rationale**: +- Simplest dependency management for Flask application +- No need for Poetry/Pipenv complexity in MVP +- Pin exact versions for reproducibility +- Virtual environment assumed for isolation + +**Core Dependencies** (estimated): +``` +Flask==3.0.0 +Flask-Login==0.6.3 +Flask-Limiter==3.5.0 +Flask-WTF==1.2.1 +anthropic==0.8.0 +clamd==1.0.2 +bcrypt==4.1.2 +PyYAML==6.0.1 +pytest==7.4.3 +pytest-flask==1.3.0 +``` + +**Development Dependencies**: +- pytest, pytest-flask for testing +- black for code formatting +- flake8 for linting + +--- + +## Technology Stack Summary + +| Component | Technology | Rationale | +|-----------|-----------|-----------| +| **Web Framework** | Flask 3.0+ | Lightweight, simple, widely supported | +| **Template Engine** | Jinja2 (built-in) | Server-side rendering, no JS needed | +| **AI Provider** | Claude API (Anthropic) | User-specified, abstracted for future flexibility | +| **Authentication** | Flask-Login + bcrypt | Standard session management, secure passwords | +| **Rate Limiting** | Flask-Limiter | Prevent abuse, simple IP-based approach | +| **Malware Scanning** | ClamAV + clamd | Open-source, reliable, on-premise | +| **Storage** | Filesystem (YAML + Markdown) | Matches user requirements, simple, no DB | +| **Testing** | pytest + pytest-flask | Industry standard, good Flask integration | +| **Python Version** | 3.11+ | Modern, stable, good performance | +| **Deployment** | Gunicorn (WSGI) | Production-ready Flask server | + +--- + +## Non-Functional Requirements Research + +### Performance Considerations + +**Concurrent Submissions** (SC-012): +- Target: 100 concurrent submissions without errors +- Flask + Gunicorn with 4-8 worker processes should handle this +- File I/O is bottleneck: consider async I/O if performance issues arise +- AI analysis happens asynchronously (background task) to not block submission response + +**Dashboard Performance** (SC-008): +- Target: Load 1000 items in <3 seconds +- File-based approach: Index product feedback directories, cache counts +- Implement pagination (50 items per page) +- Use lazy loading for file attachments (links, not embedded content) + +**AI Analysis Time** (SC-007): +- Target: <30 seconds for 95% of submissions +- Claude Haiku model typically responds in 2-5 seconds for translation/categorization +- Timeout: 45 seconds before marking as failed +- Queue-based processing if needed (Python `queue` module or simple file-based queue) + +### Security Considerations + +**Anonymity Enforcement** (FR-055, SC-010): +- Do NOT log IP addresses in feedback metadata +- Rate limiting uses IP for abuse prevention only, not stored with feedback +- No session cookies for anonymous submission +- No analytics/tracking scripts + +**File Upload Security**: +- Validate MIME types server-side (don't trust client) +- ClamAV scanning before storage +- Sanitize filenames to prevent directory traversal +- Store outside web root, serve via Flask route with access control + +**HTTPS Requirement** (FR-064): +- Deployment guide must specify reverse proxy (nginx) with TLS +- Redirect HTTP to HTTPS +- HSTS headers recommended + +--- + +## Open Questions for Implementation Phase + +1. **Asynchronous AI Analysis**: Should analysis happen synchronously (user waits) or asynchronously (background job)? + - **Recommendation**: Asynchronous - return success immediately, process in background + - Implement simple file-based queue or use Python `threading` for MVP + +2. **Admin Bootstrap**: How is the first admin user created? + - **Recommendation**: CLI command or config file initialization script + +3. **Email Notifications**: Out of scope (line 255) but commonly requested + - **Recommendation**: Document as future enhancement, design hooks for extensibility + +4. **Logging Strategy**: Structured logs for operational monitoring? + - **Recommendation**: Python `logging` module, JSON format, separate file per environment + +--- + +## Research Validation + +All NEEDS CLARIFICATION items from Technical Context have been resolved: + +| Item | Resolution | Document Section | +|------|------------|------------------| +| AI Integration | Pluggable interface, Claude as default | §1 | +| File Upload - Malware Scanning | ClamAV + clamd | §2 | +| File Upload - Storage Location | Filesystem: `data/products/.../feedback/.../attachments/` | §3 | +| Authentication | Flask-Login + server-side sessions + bcrypt | §4 | +| Rate Limiting | Flask-Limiter, 10/hour/IP | §5 | + +**Next Phase**: Proceed to Phase 1 (data-model.md, contracts, quickstart.md)