Add implementation planning artifacts for anonymous feedback platform
Complete Phase 0 (Research) and Phase 1 (Design) of implementation planning workflow: - plan.md: Technical context, constitution check, and project structure - research.md: Technology decisions for AI integration, file storage, authentication, security - data-model.md: Domain entities with validation rules and state transitions - contracts/: API specifications for submission, dashboard, and admin endpoints - quickstart.md: Developer setup guide with test-first workflow - CLAUDE.md: Updated agent context with tech stack All constitutional requirements validated. Ready for task generation phase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Product Management</title></head>
|
||||
<body>
|
||||
<h1>Product Management</h1>
|
||||
|
||||
<a href="/admin/products/new">+ Create New Product</a>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Submission URL</th>
|
||||
<th>Target Language</th>
|
||||
<th>Feedback Count</th>
|
||||
<th>Assigned Owners</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>001-acme-app</td>
|
||||
<td>Acme Mobile App</td>
|
||||
<td>Active</td>
|
||||
<td><a href="/submit/acme-app">/submit/acme-app</a></td>
|
||||
<td>English (en)</td>
|
||||
<td>127</td>
|
||||
<td>2 owners</td>
|
||||
<td>
|
||||
<a href="/admin/products/001-acme-app/edit">Edit</a> |
|
||||
<a href="/admin/products/001-acme-app/archive">Archive</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- More rows... -->
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Error (403 Forbidden)**: User is not an administrator
|
||||
```html
|
||||
Content-Type: text/html
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Access Denied</h1>
|
||||
<p>Administrator privileges required.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Create New Product</h1>
|
||||
<form method="POST" action="/admin/products">
|
||||
<label>Product ID (URL-safe):
|
||||
<input type="text" name="id" pattern="[a-z0-9-]+" required placeholder="001-my-product">
|
||||
</label>
|
||||
|
||||
<label>Name:
|
||||
<input type="text" name="name" maxlength="100" required placeholder="My Product">
|
||||
</label>
|
||||
|
||||
<label>Description:
|
||||
<textarea name="description" maxlength="500" placeholder="Optional description"></textarea>
|
||||
</label>
|
||||
|
||||
<label>Target Language for Translations:
|
||||
<select name="target_language" required>
|
||||
<option value="en">English</option>
|
||||
<option value="de">German</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="ja">Japanese</option>
|
||||
<!-- More languages... -->
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>Submission URL Slug:
|
||||
<input type="text" name="submission_url_slug" pattern="[a-z0-9-]+" required placeholder="my-product">
|
||||
</label>
|
||||
|
||||
<label>Assigned Product Owners:
|
||||
<select name="assigned_owner_ids" multiple>
|
||||
<option value="owner-001">Jane Smith (jane.smith@example.com)</option>
|
||||
<option value="owner-002">John Doe (john.doe@example.com)</option>
|
||||
<!-- More owners... -->
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button type="submit">Create Product</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Validation Error</h1>
|
||||
<ul>
|
||||
<li>Product ID must be unique</li>
|
||||
<li>Product ID must be URL-safe (lowercase, hyphens only)</li>
|
||||
<li>Submission URL slug must be unique</li>
|
||||
<li>At least one product owner must be assigned</li>
|
||||
<li>Target language must be valid ISO 639-1 code</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>User Management</h1>
|
||||
|
||||
<a href="/admin/users/new">+ Create New User</a>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Email</th>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Assigned Products</th>
|
||||
<th>Last Login</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>owner-001</td>
|
||||
<td>jane.smith@example.com</td>
|
||||
<td>Jane Smith</td>
|
||||
<td>Product Owner</td>
|
||||
<td>2 products</td>
|
||||
<td>2025-10-15 09:23</td>
|
||||
<td>
|
||||
<a href="/admin/users/owner-001/edit">Edit</a> |
|
||||
<a href="/admin/users/owner-001/delete">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- More rows... -->
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Create New User</h1>
|
||||
<form method="POST" action="/admin/users">
|
||||
<label>Email:
|
||||
<input type="email" name="email" required>
|
||||
</label>
|
||||
|
||||
<label>Name:
|
||||
<input type="text" name="name" maxlength="100" required>
|
||||
</label>
|
||||
|
||||
<label>Password:
|
||||
<input type="password" name="password" minlength="8" required>
|
||||
</label>
|
||||
|
||||
<label>Role:
|
||||
<select name="role" required>
|
||||
<option value="product_owner">Product Owner</option>
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button type="submit">Create User</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Validation Error</h1>
|
||||
<ul>
|
||||
<li>Email must be unique</li>
|
||||
<li>Password must be at least 8 characters</li>
|
||||
<li>Invalid role specified</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Cannot Delete</h1>
|
||||
<p>You cannot delete your own account.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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
|
||||
@@ -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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Login - Reklamator</title></head>
|
||||
<body>
|
||||
<h1>Login</h1>
|
||||
<form method="POST" action="/login">
|
||||
<input type="email" name="email" required placeholder="Email">
|
||||
<input type="password" name="password" required placeholder="Password">
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Login Failed</h1>
|
||||
<p>Invalid email or password.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Feedback Dashboard</title></head>
|
||||
<body>
|
||||
<h1>Feedback Dashboard</h1>
|
||||
|
||||
<!-- Product selector if multiple products assigned -->
|
||||
<select name="product">
|
||||
<option value="001-acme-app">Acme Mobile App (127 items)</option>
|
||||
<option value="002-beta-service">Beta Service (43 items)</option>
|
||||
</select>
|
||||
|
||||
<!-- Filters -->
|
||||
<form method="GET" action="/dashboard">
|
||||
<select name="category">
|
||||
<option value="">All Categories</option>
|
||||
<option value="idea">Ideas</option>
|
||||
<option value="feature_request">Feature Requests</option>
|
||||
<option value="bug">Bugs</option>
|
||||
<option value="complaint">Complaints</option>
|
||||
</select>
|
||||
<select name="status">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="analyzed">Analyzed</option>
|
||||
<option value="reviewed">Reviewed</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
<input type="text" name="search" placeholder="Search feedback...">
|
||||
<button type="submit">Filter</button>
|
||||
</form>
|
||||
|
||||
<!-- Feedback list -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Date</th>
|
||||
<th>Category</th>
|
||||
<th>Original Lang</th>
|
||||
<th>Summary</th>
|
||||
<th>Status</th>
|
||||
<th>Attachments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="/feedback/a3f2c1d5">a3f2c1d5</a></td>
|
||||
<td>2025-10-15 14:32</td>
|
||||
<td>Bug</td>
|
||||
<td>DE</td>
|
||||
<td>User reports app crashes when uploading large files...</td>
|
||||
<td>Reviewed</td>
|
||||
<td>2 files</td>
|
||||
</tr>
|
||||
<!-- More rows... -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination">
|
||||
<a href="/dashboard?page=1">1</a>
|
||||
<a href="/dashboard?page=2">2</a>
|
||||
<a href="/dashboard?page=3">3</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>No Access</h1>
|
||||
<p>You are not assigned to any products.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Feedback Detail - a3f2c1d5</title></head>
|
||||
<body>
|
||||
<h1>Feedback Detail</h1>
|
||||
|
||||
<div class="metadata">
|
||||
<p><strong>ID:</strong> a3f2c1d5-8b4e-4f1a-9c2d-7e6f5a4b3c2d</p>
|
||||
<p><strong>Product:</strong> Acme Mobile App</p>
|
||||
<p><strong>Submitted:</strong> 2025-10-15 14:32:10 UTC</p>
|
||||
<p><strong>Original Language:</strong> German (DE)</p>
|
||||
<p><strong>Category:</strong> Bug (confidence: 0.92)</p>
|
||||
<p><strong>Status:</strong>
|
||||
<form method="POST" action="/feedback/a3f2c1d5/status">
|
||||
<select name="status">
|
||||
<option value="analyzed">Analyzed</option>
|
||||
<option value="reviewed" selected>Reviewed</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
<button type="submit">Update Status</button>
|
||||
</form>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>AI Analysis Summary</h2>
|
||||
<p>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.</p>
|
||||
|
||||
<h2>Original Text (German)</h2>
|
||||
<pre>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.</pre>
|
||||
|
||||
<h2>Translation (English)</h2>
|
||||
<pre>The app crashes when I try to upload large files. Every time I upload a PDF over 5MB, the app freezes and closes.</pre>
|
||||
|
||||
<h2>Attachments</h2>
|
||||
<ul>
|
||||
<li><a href="/feedback/a3f2c1d5/attachment/screenshot.png" target="_blank">screenshot.png</a> (245 KB)</li>
|
||||
<li><a href="/feedback/a3f2c1d5/attachment/error_log.txt" target="_blank">error_log.txt</a> (1 KB)</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Access Denied</h1>
|
||||
<p>You do not have permission to view this feedback.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Error (404 Not Found)**: Feedback does not exist
|
||||
```html
|
||||
Content-Type: text/html
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Feedback Not Found</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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).
|
||||
@@ -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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Submit Feedback - {Product Name}</title></head>
|
||||
<body>
|
||||
<h1>Submit Feedback for {Product Name}</h1>
|
||||
<form method="POST" action="/submit/{product_slug}" enctype="multipart/form-data">
|
||||
<textarea name="feedback_text" maxlength="10000"></textarea>
|
||||
<input type="file" name="attachments" multiple accept=".pdf,.docx,.txt,.jpg,.png,.gif,.webp">
|
||||
<button type="submit">Submit Feedback</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Error (404 Not Found)**: Product does not exist or is archived
|
||||
```html
|
||||
Content-Type: text/html
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Product Not Found</h1>
|
||||
<p>The product you're looking for does not exist or is no longer accepting feedback.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Thank You!</h1>
|
||||
<p>Your feedback has been submitted successfully.</p>
|
||||
<p>Your feedback ID: {feedback_id}</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Error (400 Bad Request)**: Validation failure
|
||||
```html
|
||||
Content-Type: text/html
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Submission Error</h1>
|
||||
<ul>
|
||||
<li>Feedback must contain text or at least one attachment</li>
|
||||
<li>Maximum 3 attachments allowed</li>
|
||||
<li>Text cannot exceed 10,000 characters</li>
|
||||
<li>File size cannot exceed 10MB per file</li>
|
||||
<li>Unsupported file type: {filename}</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Error (413 Payload Too Large)**: File size exceeds limit
|
||||
```html
|
||||
Content-Type: text/html
|
||||
HTTP/1.1 413 Payload Too Large
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>File Too Large</h1>
|
||||
<p>One or more files exceed the 10MB limit.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Too Many Submissions</h1>
|
||||
<p>You have exceeded the submission limit of 10 per hour. Please try again later.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Error (451 Unavailable For Legal Reasons)**: Malware detected
|
||||
```html
|
||||
Content-Type: text/html
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Security Error</h1>
|
||||
<p>One or more files failed security scanning. Please ensure your files are safe and try again.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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).
|
||||
Reference in New Issue
Block a user