Add implementation plan for product selection landing page (Feature 002)
Completed planning phases 0 and 1 for simple landing page feature. ## Plan Overview: **Approach**: Minimal addition to existing Flask app - reuse Product model, add one route, one template. No new dependencies or complexity. **Constitution Check**: ✅ All 5 principles satisfied - Specification-first development (spec.md complete) - Test-first discipline (TDD workflow defined) - Independent user stories (3 stories, all independently testable) - Simplicity (reuses existing Flask/Jinja2/Product architecture) - Documentation as code (all artifacts in specs/002-product-list/) ## Artifacts Created: ### Phase 0: Research (research.md) - Reuses infrastructure from feature 001 (Flask, Jinja2, file storage) - Single new decision: Product.load_active() method for filtering/sorting - Performance analysis: <100ms for 100 products (well under 1s target) ### Phase 1: Design & Contracts - **data-model.md**: Documents Product model extension (load_active method) - **contracts/landing-page.yaml**: OpenAPI contract for GET / route - **quickstart.md**: Developer implementation guide with: - Step-by-step implementation checklist - Code snippets for route, template, tests - TDD workflow (write tests → verify fail → implement → pass) - Manual verification checklist ### Agent Context - Updated CLAUDE.md with feature technologies (no new tech added) ## Implementation Summary: **New Files** (to be created): - app/routes/landing.py - Landing page route handler - app/templates/landing/index.html - Product list template - tests/contract/test_landing_routes.py - Contract tests (6 scenarios) - tests/integration/test_landing_flow.py - User journey test **Modified Files**: - app/models/product.py - Add load_active() class method - app/__init__.py - Register landing blueprint ## Key Technical Decisions: 1. **Filtering**: status=='active' AND submission_url_slug exists 2. **Sorting**: Alphabetical by name (case-insensitive), then product_id 3. **Empty State**: "No products are currently accepting feedback" message 4. **XSS Prevention**: Jinja2 auto-escaping (no manual escaping needed) 5. **Performance**: File I/O sufficient (<1s for 100 products, no caching) ## Next Steps: 1. Run /speckit.tasks to generate tasks.md 2. Run /speckit.implement to execute TDD workflow 3. Verify all tests pass 4. Manual verification checklist 5. Create pull request 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ Auto-generated from all feature plans. Last updated: 2025-10-15
|
|||||||
|
|
||||||
## Active Technologies
|
## Active Technologies
|
||||||
- Python 3.11+ + Flask (web framework), no CSS frameworks, no JavaScript libraries (001-build-an-application)
|
- Python 3.11+ + Flask (web framework), no CSS frameworks, no JavaScript libraries (001-build-an-application)
|
||||||
|
- Python 3.11+ + Flask 3.0+, Jinja2 (built-in) (002-product-list)
|
||||||
|
- File-based (data/products/*/config.yaml - existing) (002-product-list)
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
```
|
```
|
||||||
@@ -19,6 +21,7 @@ cd src [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLO
|
|||||||
Python 3.11+: Follow standard conventions
|
Python 3.11+: Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- 002-product-list: Added Python 3.11+ + Flask 3.0+, Jinja2 (built-in)
|
||||||
- 001-build-an-application: Added Python 3.11+ + Flask (web framework), no CSS frameworks, no JavaScript libraries
|
- 001-build-an-application: Added Python 3.11+ + Flask (web framework), no CSS frameworks, no JavaScript libraries
|
||||||
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: Reklamator Landing Page API
|
||||||
|
version: 1.0.0
|
||||||
|
description: Product selection landing page contract for feature 002-product-list
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
summary: Landing page - list active products
|
||||||
|
description: |
|
||||||
|
Display a landing page showing all active products available for feedback submission.
|
||||||
|
Products are sorted alphabetically by name (case-insensitive), with product_id as tiebreaker.
|
||||||
|
operationId: getLandingPage
|
||||||
|
tags:
|
||||||
|
- Landing Page
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: HTML page with product list or empty state message
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Server-rendered HTML page
|
||||||
|
examples:
|
||||||
|
with_products:
|
||||||
|
summary: Multiple active products displayed
|
||||||
|
value: |
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Select a Product</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Select a Product for Feedback</h1>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="/submit/acme-app">Acme Application</a>
|
||||||
|
<p>Enterprise resource planning system</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/submit/beta-service">Beta Service</a>
|
||||||
|
<p>Cloud infrastructure platform</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
no_products:
|
||||||
|
summary: No active products (empty state)
|
||||||
|
value: |
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Select a Product</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Select a Product for Feedback</h1>
|
||||||
|
<p>No products are currently accepting feedback. Please check back later.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
product_without_description:
|
||||||
|
summary: Product without description (no placeholder text)
|
||||||
|
value: |
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Select a Product</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Select a Product for Feedback</h1>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="/submit/simple-app">Simple App</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
# No request/response schemas needed (HTML rendering)
|
||||||
|
# Product data comes from file-based storage, not API request body
|
||||||
|
|
||||||
|
# Contract Test Scenarios
|
||||||
|
# These scenarios should be covered in tests/contract/test_landing_routes.py:
|
||||||
|
#
|
||||||
|
# 1. GET / with active products → 200 OK with product list HTML
|
||||||
|
# 2. GET / with no active products → 200 OK with empty state message
|
||||||
|
# 3. GET / with mixed active/archived → 200 OK showing only active
|
||||||
|
# 4. GET / verifies alphabetical sorting (name, then product_id)
|
||||||
|
# 5. GET / excludes products with missing submission_url_slug
|
||||||
|
# 6. GET / properly escapes product names (XSS prevention)
|
||||||
|
# 7. GET / displays descriptions when present
|
||||||
|
# 8. GET / omits description placeholder when missing
|
||||||
|
# 9. GET / accessible to anonymous users
|
||||||
|
# 10. GET / accessible to authenticated users (same behavior)
|
||||||
|
|
||||||
|
# Success Criteria Validation:
|
||||||
|
# - SC-001: Page contains clickable links to /submit/{slug}
|
||||||
|
# - SC-002: Response time <1s for up to 100 products
|
||||||
|
# - SC-004: Existing /submit/{slug} routes still functional (backwards compatibility)
|
||||||
|
# - SC-005: Empty state message displayed when no active products
|
||||||
|
# - SC-006: HTML escaping prevents XSS (test with <script> in product name)
|
||||||
|
# - SC-007: No JavaScript in response (server-side rendered only)
|
||||||
|
# - SC-008: Visual separation via HTML list structure
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# Data Model: Product Selection Landing Page
|
||||||
|
|
||||||
|
**Feature**: 002-product-list | **Date**: 2025-10-17
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This feature reuses the existing **Product** entity from feature 001-build-an-application with a minor extension. No new entities are created.
|
||||||
|
|
||||||
|
## Existing Entity: Product
|
||||||
|
|
||||||
|
**Source**: `app/models/product.py` (from feature 001)
|
||||||
|
|
||||||
|
**Attributes** (relevant to this feature):
|
||||||
|
|
||||||
|
| Attribute | Type | Required | Description |
|
||||||
|
|-----------|------|----------|-------------|
|
||||||
|
| `product_id` | str | Yes | Unique identifier (e.g., "001-acme-app") |
|
||||||
|
| `name` | str | Yes | Display name for the product |
|
||||||
|
| `submission_url_slug` | str | Yes | URL segment for submission form |
|
||||||
|
| `status` | str | Yes | "active" or "archived" |
|
||||||
|
| `description` | str | No | Brief product description (optional) |
|
||||||
|
|
||||||
|
**Storage**: File-based YAML at `data/products/{product-id}/config.yaml`
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```yaml
|
||||||
|
product_id: "001-acme-app"
|
||||||
|
name: "Acme Application"
|
||||||
|
submission_url_slug: "acme-app"
|
||||||
|
status: "active"
|
||||||
|
description: "Enterprise resource planning system"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Model Extension
|
||||||
|
|
||||||
|
### New Class Method: `load_active()`
|
||||||
|
|
||||||
|
**Purpose**: Load and return only active products with valid submission URLs, sorted for display.
|
||||||
|
|
||||||
|
**Signature**:
|
||||||
|
```python
|
||||||
|
@classmethod
|
||||||
|
def load_active(cls) -> list[Product]:
|
||||||
|
"""Load all active products, sorted alphabetically by name then product_id."""
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns**: List of Product instances where:
|
||||||
|
- `status == 'active'`
|
||||||
|
- `submission_url_slug` is present and valid
|
||||||
|
- Sorted by: `(name.lower(), product_id)`
|
||||||
|
|
||||||
|
**Implementation Location**: `app/models/product.py`
|
||||||
|
|
||||||
|
**Usage Example**:
|
||||||
|
```python
|
||||||
|
from app.models.product import Product
|
||||||
|
|
||||||
|
# In route handler
|
||||||
|
products = Product.load_active()
|
||||||
|
# Returns sorted list of active products ready for display
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
### Landing Page Load Sequence
|
||||||
|
|
||||||
|
1. **Request**: User navigates to `/`
|
||||||
|
2. **Route Handler**: `app/routes/landing.py`
|
||||||
|
- Calls `Product.load_active()`
|
||||||
|
3. **Data Loading**: Product model
|
||||||
|
- Reads all `data/products/*/config.yaml` files
|
||||||
|
- Filters: `status == 'active'` AND `submission_url_slug` exists
|
||||||
|
- Sorts: By `(name.lower(), product_id)`
|
||||||
|
4. **Response**: Render template with product list
|
||||||
|
- Pass `products` to `templates/landing/index.html`
|
||||||
|
- Template loops over products, displays name/description/link
|
||||||
|
|
||||||
|
### Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
User → GET / → landing.py → Product.load_active() → [Product, Product, ...]
|
||||||
|
↓
|
||||||
|
Templates (Jinja2) → HTML Response → User
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
|
||||||
|
### Product Visibility (for landing page)
|
||||||
|
|
||||||
|
A product is **visible** on the landing page if and only if:
|
||||||
|
1. `status == 'active'` (FR-003)
|
||||||
|
2. `submission_url_slug` is not None/empty (FR-015)
|
||||||
|
|
||||||
|
Products failing either condition are **excluded** from the list.
|
||||||
|
|
||||||
|
### Sorting Rules (FR-009)
|
||||||
|
|
||||||
|
Products are sorted by:
|
||||||
|
1. **Primary**: `name` (case-insensitive alphabetical)
|
||||||
|
2. **Secondary**: `product_id` (alphabetical, for ties)
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- Input: `[{"name": "Zebra", "product_id": "001"}, {"name": "Apple", "product_id": "002"}]`
|
||||||
|
- Output: `[{"name": "Apple", ...}, {"name": "Zebra", ...}]`
|
||||||
|
|
||||||
|
- Input: `[{"name": "App", "product_id": "002"}, {"name": "App", "product_id": "001"}]`
|
||||||
|
- Output: `[{"name": "App", "product_id": "001"}, {"name": "App", "product_id": "002"}]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## No New Entities
|
||||||
|
|
||||||
|
This feature introduces **no new entities**. All data structures are reused from feature 001:
|
||||||
|
- Product entity (extended with `load_active()` method only)
|
||||||
|
- File-based YAML storage (unchanged)
|
||||||
|
- No database tables, no new data files
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Considerations
|
||||||
|
|
||||||
|
### Test Data Requirements
|
||||||
|
|
||||||
|
For comprehensive testing, create products with:
|
||||||
|
- Various statuses: "active", "archived"
|
||||||
|
- With/without descriptions
|
||||||
|
- With/without valid `submission_url_slug`
|
||||||
|
- Duplicate names (to test secondary sort)
|
||||||
|
- Various name cases ("Apple", "apple", "APPLE")
|
||||||
|
|
||||||
|
### Expected Behaviors
|
||||||
|
|
||||||
|
| Scenario | Expected Result |
|
||||||
|
|----------|----------------|
|
||||||
|
| Product with `status: active` | Included in list |
|
||||||
|
| Product with `status: archived` | Excluded from list |
|
||||||
|
| Product with missing `submission_url_slug` | Excluded from list |
|
||||||
|
| Products with same name | Sorted by product_id |
|
||||||
|
| Empty product directory | Returns empty list |
|
||||||
|
|
||||||
|
**Reference Tests**: See `tests/contract/test_landing_routes.py` for data model validation tests.
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# Implementation Plan: Product Selection Landing Page
|
||||||
|
|
||||||
|
**Branch**: `002-product-list` | **Date**: 2025-10-17 | **Spec**: [spec.md](./spec.md)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add a landing page at root URL (`/`) that lists all active products, enabling visitors to discover and select products for feedback submission. This removes the barrier of requiring users to know direct product URLs.
|
||||||
|
|
||||||
|
**Technical approach**: Add new Flask route, reuse existing Product model, create simple HTML template with server-side rendering. No new dependencies needed.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: Python 3.11+
|
||||||
|
**Primary Dependencies**: Flask 3.0+, Jinja2 (built-in)
|
||||||
|
**Storage**: File-based (data/products/*/config.yaml - existing)
|
||||||
|
**Testing**: pytest + pytest-flask (existing)
|
||||||
|
**Target Platform**: Linux server (existing deployment)
|
||||||
|
**Project Type**: Web application (Flask backend with server-side rendering)
|
||||||
|
**Performance Goals**: <1 second page load for up to 100 products
|
||||||
|
**Constraints**: Server-side rendering only (no JavaScript), minimal CSS (no frameworks)
|
||||||
|
**Scale/Scope**: Simple single-page addition to existing Flask app
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before Phase 0 research.*
|
||||||
|
|
||||||
|
### Principle I: Specification-First Development
|
||||||
|
✅ **PASS** - Complete specification exists at spec.md with prioritized user stories, functional requirements, and success criteria.
|
||||||
|
|
||||||
|
### Principle II: Test-First Discipline
|
||||||
|
✅ **PASS** - Implementation will follow TDD: contract tests → integration tests → implementation.
|
||||||
|
|
||||||
|
### Principle III: Independent User Stories
|
||||||
|
✅ **PASS** - All 3 user stories (P1: Browse/Select, P2: Status visibility, P3: Direct nav) are independently testable and deliverable.
|
||||||
|
|
||||||
|
### Principle IV: Simplicity & Justification
|
||||||
|
✅ **PASS** - Feature reuses existing architecture (Flask routes, Product model, Jinja2 templates). No new abstractions, dependencies, or complexity added.
|
||||||
|
|
||||||
|
### Principle V: Documentation as Code
|
||||||
|
✅ **PASS** - Specification, plan, and implementation artifacts maintained in specs/002-product-list/ with version control.
|
||||||
|
|
||||||
|
**Constitution Status**: ✅ All principles satisfied. No violations to justify.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```
|
||||||
|
specs/002-product-list/
|
||||||
|
├── spec.md # Feature specification (complete)
|
||||||
|
├── plan.md # This file
|
||||||
|
├── research.md # Phase 0 - Technical research (minimal - reuses 001)
|
||||||
|
├── data-model.md # Phase 1 - Data model (reference to existing Product)
|
||||||
|
├── contracts/ # Phase 1 - API contract (GET / route)
|
||||||
|
│ └── landing-page.yaml
|
||||||
|
├── quickstart.md # Phase 1 - Developer quickstart
|
||||||
|
└── tasks.md # Phase 2 - Task breakdown (/speckit.tasks)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── models/
|
||||||
|
│ └── product.py # Existing - no changes needed
|
||||||
|
├── routes/
|
||||||
|
│ └── landing.py # NEW - landing page route
|
||||||
|
└── templates/
|
||||||
|
└── landing/
|
||||||
|
└── index.html # NEW - product list template
|
||||||
|
|
||||||
|
tests/
|
||||||
|
├── contract/
|
||||||
|
│ └── test_landing_routes.py # NEW - contract tests for GET /
|
||||||
|
└── integration/
|
||||||
|
└── test_landing_flow.py # NEW - end-to-end user journey tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Reuse existing Flask application structure. Landing page is a simple addition: one new route file, one new template, and corresponding tests. Follows established patterns from feature 001.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
*No violations - table not needed.*
|
||||||
|
|
||||||
|
All complexity requirements from Constitution Principle IV are satisfied:
|
||||||
|
- No additional abstraction layers
|
||||||
|
- No new dependencies
|
||||||
|
- No new design patterns
|
||||||
|
- Reuses existing Flask/Jinja2/Product architecture
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0: Research
|
||||||
|
|
||||||
|
### Research Scope
|
||||||
|
|
||||||
|
Since this feature builds on existing infrastructure from 001-build-an-application, minimal research is needed. Key questions already answered:
|
||||||
|
|
||||||
|
1. **Product data access**: Resolved in 001 - Product.load_all() method exists
|
||||||
|
2. **Template rendering**: Resolved in 001 - Jinja2 with server-side rendering
|
||||||
|
3. **Route patterns**: Resolved in 001 - Flask blueprints for organization
|
||||||
|
4. **Sorting implementation**: Python built-in sorted() with key function
|
||||||
|
|
||||||
|
### New Technical Decisions
|
||||||
|
|
||||||
|
Only one new decision needed for this feature:
|
||||||
|
|
||||||
|
**Product List Retrieval & Sorting**
|
||||||
|
- Decision: Extend existing Product model with `load_active()` class method
|
||||||
|
- Rationale: Centralizes "active products only" logic, enables reuse
|
||||||
|
- Sorting: Python's `sorted()` with `key=lambda p: (p.name.lower(), p.product_id)`
|
||||||
|
- Performance: File I/O for 100 products ~10-50ms (acceptable for <1s target)
|
||||||
|
|
||||||
|
**Output**: research.md (minimal - references 001, documents sorting decision)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Design & Contracts
|
||||||
|
|
||||||
|
### Data Model
|
||||||
|
|
||||||
|
**Entities**: Reuse existing Product model from 001-build-an-application
|
||||||
|
|
||||||
|
**Extension needed**:
|
||||||
|
```python
|
||||||
|
# app/models/product.py - add class method
|
||||||
|
@classmethod
|
||||||
|
def load_active(cls):
|
||||||
|
"""Load all active products, sorted alphabetically by name then product_id"""
|
||||||
|
all_products = cls.load_all()
|
||||||
|
active = [p for p in all_products if p.status == 'active' and p.submission_url_slug]
|
||||||
|
return sorted(active, key=lambda p: (p.name.lower(), p.product_id))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: data-model.md (references existing Product entity, documents extension)
|
||||||
|
|
||||||
|
### API Contracts
|
||||||
|
|
||||||
|
**New Route**: `GET /`
|
||||||
|
|
||||||
|
**Contract**:
|
||||||
|
```yaml
|
||||||
|
# contracts/landing-page.yaml
|
||||||
|
paths:
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
summary: Landing page - list active products
|
||||||
|
operationId: getLandingPage
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: HTML page with product list
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
examples:
|
||||||
|
with_products:
|
||||||
|
summary: Multiple active products
|
||||||
|
value: |
|
||||||
|
<html>
|
||||||
|
<h1>Select a Product</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/submit/product-a">Product A</a> - Description</li>
|
||||||
|
<li><a href="/submit/product-b">Product B</a></li>
|
||||||
|
</ul>
|
||||||
|
</html>
|
||||||
|
no_products:
|
||||||
|
summary: No active products
|
||||||
|
value: |
|
||||||
|
<html>
|
||||||
|
<p>No products are currently accepting feedback.</p>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: contracts/landing-page.yaml
|
||||||
|
|
||||||
|
### Developer Quickstart
|
||||||
|
|
||||||
|
Key implementation points for developers:
|
||||||
|
|
||||||
|
1. **Route**: app/routes/landing.py with `@app.route('/')`
|
||||||
|
2. **Template**: app/templates/landing/index.html - loop over products
|
||||||
|
3. **XSS Prevention**: Use Jinja2 auto-escaping for product names/descriptions
|
||||||
|
4. **Empty State**: Check `if products` to show appropriate message
|
||||||
|
5. **Logging**: Log landing page access with product count
|
||||||
|
|
||||||
|
**Output**: quickstart.md
|
||||||
|
|
||||||
|
### Agent Context Update
|
||||||
|
|
||||||
|
Run: `.specify/scripts/bash/update-agent-context.sh claude`
|
||||||
|
|
||||||
|
**Expected update**: No new technologies added (reuses Flask, Jinja2, Python 3.11+)
|
||||||
|
|
||||||
|
**Output**: Updated .claude.md or equivalent agent context file
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Task Generation
|
||||||
|
|
||||||
|
**Not executed by /speckit.plan** - run `/speckit.tasks` next.
|
||||||
|
|
||||||
|
Expected task structure:
|
||||||
|
1. Contract tests for GET / (various scenarios)
|
||||||
|
2. Integration test for user journey
|
||||||
|
3. Implement Product.load_active() method
|
||||||
|
4. Implement landing route
|
||||||
|
5. Create landing template
|
||||||
|
6. Add logging
|
||||||
|
7. Manual verification
|
||||||
|
|
||||||
|
**Output**: tasks.md (generated by /speckit.tasks command)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ Phase 0 complete: Generate research.md
|
||||||
|
2. ✅ Phase 1 complete: Generate data-model.md, contracts/, quickstart.md
|
||||||
|
3. ⏭️ Run `/speckit.tasks` to generate tasks.md
|
||||||
|
4. ⏭️ Run `/speckit.implement` to execute tasks
|
||||||
|
|
||||||
|
**Branch**: 002-product-list
|
||||||
|
**Plan**: /home/markus/workspace/reklamator/specs/002-product-list/plan.md
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
# Quickstart: Product Selection Landing Page
|
||||||
|
|
||||||
|
**Feature**: 002-product-list | **For**: Developers implementing this feature
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Add a landing page at `/` that lists all active products for feedback submission. This is a simple addition to the existing Flask app: one route, one template, and corresponding tests.
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### 1. Extend Product Model
|
||||||
|
|
||||||
|
**File**: `app/models/product.py`
|
||||||
|
|
||||||
|
**Add this class method**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@classmethod
|
||||||
|
def load_active(cls):
|
||||||
|
"""Load all active products, sorted alphabetically by name then product_id.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Product]: Active products with valid submission_url_slug, sorted.
|
||||||
|
"""
|
||||||
|
all_products = cls.load_all()
|
||||||
|
active = [p for p in all_products
|
||||||
|
if p.status == 'active' and p.submission_url_slug]
|
||||||
|
return sorted(active, key=lambda p: (p.name.lower(), p.product_id))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Centralizes filtering and sorting logic. Reusable if other features need active product lists.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Create Landing Route
|
||||||
|
|
||||||
|
**File**: `app/routes/landing.py` (new file)
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask import Blueprint, render_template, current_app
|
||||||
|
from app.models.product import Product
|
||||||
|
|
||||||
|
landing_bp = Blueprint('landing', __name__)
|
||||||
|
|
||||||
|
@landing_bp.route('/')
|
||||||
|
def index():
|
||||||
|
"""Landing page showing all active products."""
|
||||||
|
try:
|
||||||
|
products = Product.load_active()
|
||||||
|
current_app.logger.info(
|
||||||
|
f'Landing page accessed: {len(products)} active products'
|
||||||
|
)
|
||||||
|
return render_template('landing/index.html', products=products)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f'Error loading landing page: {e}', exc_info=True)
|
||||||
|
return render_template('landing/index.html', products=[])
|
||||||
|
```
|
||||||
|
|
||||||
|
**Register blueprint** in `app/__init__.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.routes.landing import landing_bp
|
||||||
|
app.register_blueprint(landing_bp)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Create Landing Template
|
||||||
|
|
||||||
|
**File**: `app/templates/landing/index.html` (new file)
|
||||||
|
|
||||||
|
**Template structure**:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Select a Product - Reklamator</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
|
||||||
|
h1 { color: #333; }
|
||||||
|
ul { list-style: none; padding: 0; }
|
||||||
|
li { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 4px; }
|
||||||
|
a { font-size: 1.2em; color: #0066cc; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
p { margin: 5px 0 0 0; color: #666; }
|
||||||
|
.empty-state { color: #666; padding: 20px; text-align: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Select a Product for Feedback</h1>
|
||||||
|
|
||||||
|
{% if products %}
|
||||||
|
<ul>
|
||||||
|
{% for product in products %}
|
||||||
|
<li>
|
||||||
|
<a href="/submit/{{ product.submission_url_slug }}">
|
||||||
|
{{ product.name }}
|
||||||
|
</a>
|
||||||
|
{% if product.description %}
|
||||||
|
<p>{{ product.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="empty-state">
|
||||||
|
No products are currently accepting feedback. Please check back later.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key points**:
|
||||||
|
- Jinja2 auto-escaping prevents XSS (product.name, product.description)
|
||||||
|
- No JavaScript (pure server-side rendering)
|
||||||
|
- Minimal inline CSS (no frameworks)
|
||||||
|
- Conditional rendering for empty state
|
||||||
|
- Only shows description if present (no placeholder text)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Write Contract Tests
|
||||||
|
|
||||||
|
**File**: `tests/contract/test_landing_routes.py` (new file)
|
||||||
|
|
||||||
|
**Test scenarios to implement**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
from app.models.product import Product
|
||||||
|
|
||||||
|
@pytest.mark.contract
|
||||||
|
def test_get_landing_page_with_products(client, temp_data_dir):
|
||||||
|
"""T301: GET / returns 200 with product list"""
|
||||||
|
# Setup: Create 2 active products
|
||||||
|
# Assert: 200 OK, both products in HTML
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.mark.contract
|
||||||
|
def test_get_landing_page_no_products(client, temp_data_dir):
|
||||||
|
"""T302: GET / with no active products shows empty state"""
|
||||||
|
# Assert: 200 OK, contains "No products are currently accepting feedback"
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.mark.contract
|
||||||
|
def test_get_landing_page_filters_archived(client, temp_data_dir):
|
||||||
|
"""T303: GET / excludes archived products"""
|
||||||
|
# Setup: 1 active, 1 archived
|
||||||
|
# Assert: Only active product shown
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.mark.contract
|
||||||
|
def test_get_landing_page_sorting(client, temp_data_dir):
|
||||||
|
"""T304: GET / sorts products alphabetically (name, then product_id)"""
|
||||||
|
# Setup: Products with names "Zebra", "Apple", "apple" (different product_ids)
|
||||||
|
# Assert: Correct alphabetical order
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.mark.contract
|
||||||
|
def test_get_landing_page_xss_prevention(client, temp_data_dir):
|
||||||
|
"""T305: GET / escapes HTML in product names"""
|
||||||
|
# Setup: Product with name "<script>alert('xss')</script>"
|
||||||
|
# Assert: HTML is escaped, script not executed
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.mark.contract
|
||||||
|
def test_get_landing_page_missing_slug(client, temp_data_dir):
|
||||||
|
"""T306: GET / excludes products with missing submission_url_slug"""
|
||||||
|
# Setup: Product with submission_url_slug = None
|
||||||
|
# Assert: Product not shown in list
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Write Integration Tests
|
||||||
|
|
||||||
|
**File**: `tests/integration/test_landing_flow.py` (new file)
|
||||||
|
|
||||||
|
**User journey test**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_landing_to_submission_flow(client, temp_data_dir):
|
||||||
|
"""T307: Complete flow - landing page → product selection → submission form"""
|
||||||
|
# Step 1: Visit landing page, see products
|
||||||
|
# Step 2: Click product link
|
||||||
|
# Step 3: Verify redirected to /submit/{slug}
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Implementation Notes
|
||||||
|
|
||||||
|
### XSS Prevention
|
||||||
|
- ✅ Jinja2 auto-escaping handles product names and descriptions
|
||||||
|
- ✅ No manual HTML escaping needed
|
||||||
|
- ✅ Test with `<script>` tags in product names to verify
|
||||||
|
|
||||||
|
### Empty State Handling
|
||||||
|
- ✅ Check `{% if products %}` in template
|
||||||
|
- ✅ Display message: "No products are currently accepting feedback. Please check back later."
|
||||||
|
- ✅ No blank page or error
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- ✅ Log landing page access with product count
|
||||||
|
- ✅ Log errors if product loading fails
|
||||||
|
- ✅ Use `current_app.logger.info()` for access logs
|
||||||
|
|
||||||
|
### Backwards Compatibility
|
||||||
|
- ✅ Existing `/submit/{slug}` routes unchanged
|
||||||
|
- ✅ Direct product URLs still work
|
||||||
|
- ✅ Landing page is additive only
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- ✅ Target: <1 second for up to 100 products
|
||||||
|
- ✅ File I/O ~10-50ms for 100 YAML files
|
||||||
|
- ✅ No caching needed for MVP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Workflow (TDD)
|
||||||
|
|
||||||
|
**Follow this order** (Constitution Principle II):
|
||||||
|
|
||||||
|
1. **Write contract tests** (test_landing_routes.py) - all should FAIL
|
||||||
|
2. **Verify tests fail** - proves they test something meaningful
|
||||||
|
3. **Implement Product.load_active()** method
|
||||||
|
4. **Implement landing route** (landing.py)
|
||||||
|
5. **Create landing template** (index.html)
|
||||||
|
6. **Run tests** - contract tests should PASS
|
||||||
|
7. **Write integration tests** (test_landing_flow.py) - should FAIL
|
||||||
|
8. **Fix any issues** - integration tests should PASS
|
||||||
|
9. **Refactor** while keeping tests green
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Verification Checklist
|
||||||
|
|
||||||
|
After all tests pass, manually verify:
|
||||||
|
|
||||||
|
- [ ] Visit `/` - see product list or empty state
|
||||||
|
- [ ] Click product link - redirected to `/submit/{slug}`
|
||||||
|
- [ ] Check with 0 active products - see empty message
|
||||||
|
- [ ] Check with 1 active product - see single product
|
||||||
|
- [ ] Check with 10+ active products - alphabetical order
|
||||||
|
- [ ] Check product with no description - no placeholder text
|
||||||
|
- [ ] Check product with long name - proper wrapping
|
||||||
|
- [ ] Check as anonymous user - page accessible
|
||||||
|
- [ ] Check as authenticated user - same page shown
|
||||||
|
- [ ] Check page source - no JavaScript present
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Completion Criteria
|
||||||
|
|
||||||
|
✅ All contract tests passing
|
||||||
|
✅ All integration tests passing
|
||||||
|
✅ Product.load_active() method implemented
|
||||||
|
✅ Landing route registered and functional
|
||||||
|
✅ Landing template created with proper escaping
|
||||||
|
✅ Manual verification completed
|
||||||
|
✅ Code follows existing Flask/Jinja2 patterns
|
||||||
|
✅ No new dependencies added
|
||||||
|
✅ Documentation updated (this file)
|
||||||
|
|
||||||
|
**Next**: Commit to branch `002-product-list` and create pull request
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Research: Product Selection Landing Page
|
||||||
|
|
||||||
|
**Branch**: `002-product-list` | **Date**: 2025-10-17
|
||||||
|
|
||||||
|
This document addresses technical decisions for the product selection landing page feature. Most infrastructure decisions were resolved in feature 001-build-an-application and are reused here.
|
||||||
|
|
||||||
|
## Existing Infrastructure (from 001-build-an-application)
|
||||||
|
|
||||||
|
The following technical decisions from feature 001 are reused without modification:
|
||||||
|
|
||||||
|
- **Flask 3.0+ with Jinja2**: Server-side rendering, no JavaScript
|
||||||
|
- **File-based storage**: Product configs in `data/products/*/config.yaml`
|
||||||
|
- **Product model**: Existing `app/models/product.py` with load methods
|
||||||
|
- **Template patterns**: Minimal HTML/CSS, Jinja2 auto-escaping for XSS prevention
|
||||||
|
- **Routing**: Flask route decorators, blueprint organization
|
||||||
|
- **Testing**: pytest + pytest-flask for contract and integration tests
|
||||||
|
|
||||||
|
**Reference**: See `/home/markus/workspace/reklamator/specs/001-build-an-application/research.md` for full details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New Technical Decision: Product List Retrieval & Sorting
|
||||||
|
|
||||||
|
### Decision: Extend Product model with `load_active()` class method
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Centralizes "active products only" filtering logic
|
||||||
|
- Enables reuse if other features need active product lists
|
||||||
|
- Encapsulates sorting algorithm in one place
|
||||||
|
- Follows existing Product model pattern (e.g., `load_all()`, `load_by_id()`)
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# app/models/product.py - add class method
|
||||||
|
@classmethod
|
||||||
|
def load_active(cls):
|
||||||
|
"""Load all active products, sorted alphabetically by name then product_id.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Product]: Active products with valid submission_url_slug, sorted by:
|
||||||
|
1. name (case-insensitive alphabetical)
|
||||||
|
2. product_id (alphabetical) as tiebreaker
|
||||||
|
|
||||||
|
Products with missing/invalid submission_url_slug are excluded.
|
||||||
|
"""
|
||||||
|
all_products = cls.load_all()
|
||||||
|
active = [p for p in all_products
|
||||||
|
if p.status == 'active' and p.submission_url_slug]
|
||||||
|
return sorted(active, key=lambda p: (p.name.lower(), p.product_id))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sorting Algorithm**:
|
||||||
|
- Primary sort: Product name (case-insensitive) - ensures alphabetical display
|
||||||
|
- Secondary sort: Product ID - provides stable ordering when names are identical
|
||||||
|
- Uses Python's built-in `sorted()` with tuple key for multi-level sorting
|
||||||
|
|
||||||
|
**Performance Analysis**:
|
||||||
|
- File I/O for 100 products: ~10-50ms (depends on disk speed)
|
||||||
|
- In-memory sorting: <1ms for 100 items
|
||||||
|
- Total expected latency: <100ms (well under 1-second SC-002 target)
|
||||||
|
- No caching needed for MVP (file reads are sufficiently fast)
|
||||||
|
|
||||||
|
**Filtering Logic**:
|
||||||
|
- `status == 'active'`: Per FR-003, only show active products
|
||||||
|
- `submission_url_slug`: Per FR-015, skip products with missing/invalid slugs
|
||||||
|
- Combined with `and` operator: both conditions must be true
|
||||||
|
|
||||||
|
**Alternatives Considered**:
|
||||||
|
|
||||||
|
1. **Sort in route handler**: Simpler but violates DRY if multiple routes need sorted product lists
|
||||||
|
2. **Database query with ORDER BY**: Contradicts file-based architecture decision from 001
|
||||||
|
3. **Pre-sorted cache**: Premature optimization - file reads are fast enough for 100 products
|
||||||
|
4. **Client-side sorting with JavaScript**: Violates no-JavaScript constraint from spec
|
||||||
|
|
||||||
|
**Edge Cases Handled**:
|
||||||
|
- No active products → Returns empty list (handled in template)
|
||||||
|
- Missing submission_url_slug → Product excluded from list (per FR-015)
|
||||||
|
- Identical product names → Sorted by product_id as tiebreaker
|
||||||
|
- Case-insensitive sorting → "Apple" and "apple" sort together
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
All technical decisions align with:
|
||||||
|
- **FR-002**: Retrieves from file-based storage ✅
|
||||||
|
- **FR-003**: Filters for active status ✅
|
||||||
|
- **FR-009**: Sorts alphabetically with tiebreaker ✅
|
||||||
|
- **FR-015**: Skips invalid submission_url_slug ✅
|
||||||
|
- **SC-002**: <1 second load time for 100 products ✅
|
||||||
|
|
||||||
|
**Next Phase**: Proceed to Phase 1 (data-model.md, contracts, quickstart.md)
|
||||||
@@ -5,6 +5,16 @@
|
|||||||
**Status**: Draft
|
**Status**: Draft
|
||||||
**Input**: User description: "At the moment visitors of the website need to know the link to the product submission page when they want to submit a feedback. The now should be able to see a list of all active products in order to choose what product they want to give a feedback."
|
**Input**: User description: "At the moment visitors of the website need to know the link to the product submission page when they want to submit a feedback. The now should be able to see a list of all active products in order to choose what product they want to give a feedback."
|
||||||
|
|
||||||
|
## Clarifications
|
||||||
|
|
||||||
|
### Session 2025-10-17
|
||||||
|
|
||||||
|
- Q: When a product configuration has a missing or invalid `submission_url_slug`, how should the landing page handle it? → A: Skip the product silently and log an error (user sees only valid products)
|
||||||
|
- Q: How should archived products be handled on the landing page? → A: Do not display archived products at all (only show active products)
|
||||||
|
- Q: When an authenticated product owner or admin accesses the landing page at `/`, what should happen? → A: Show the landing page normally (authentication doesn't affect access)
|
||||||
|
- Q: When a product has no description field (or it's empty), what should be displayed? → A: Display product name only (no description text shown)
|
||||||
|
- Q: When multiple products have identical names, how should they be sorted in the alphabetical list? → A: Order by product_id alphabetically as secondary sort
|
||||||
|
|
||||||
## User Scenarios & Testing *(mandatory)*
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
### User Story 1 - Browse and Select Product (Priority: P1)
|
### User Story 1 - Browse and Select Product (Priority: P1)
|
||||||
@@ -17,7 +27,7 @@ A visitor arrives at the Reklamator platform without knowing the specific produc
|
|||||||
|
|
||||||
**Acceptance Scenarios**:
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
1. **Given** a visitor arrives at the platform root URL (`/`), **When** they view the page, **Then** they see a list of all active products with product names and brief descriptions
|
1. **Given** a visitor arrives at the platform root URL (`/`), **When** they view the page, **Then** they see a list of all active products with product names (and descriptions if available)
|
||||||
2. **Given** multiple active products exist in the system, **When** a visitor views the landing page, **Then** all active products are displayed in a clear, organized list
|
2. **Given** multiple active products exist in the system, **When** a visitor views the landing page, **Then** all active products are displayed in a clear, organized list
|
||||||
3. **Given** a visitor sees the product list, **When** they click on a product name or "Submit Feedback" button, **Then** they are redirected to that product's submission form (`/submit/{product-slug}`)
|
3. **Given** a visitor sees the product list, **When** they click on a product name or "Submit Feedback" button, **Then** they are redirected to that product's submission form (`/submit/{product-slug}`)
|
||||||
4. **Given** a product has a descriptive summary, **When** displayed on the landing page, **Then** the summary helps the visitor understand what the product is
|
4. **Given** a product has a descriptive summary, **When** displayed on the landing page, **Then** the summary helps the visitor understand what the product is
|
||||||
@@ -31,13 +41,13 @@ A visitor wants to understand which products are currently accepting feedback an
|
|||||||
|
|
||||||
**Why this priority**: Provides transparency about product status and prevents user frustration. This is secondary to basic discovery but improves user experience by setting clear expectations.
|
**Why this priority**: Provides transparency about product status and prevents user frustration. This is secondary to basic discovery but improves user experience by setting clear expectations.
|
||||||
|
|
||||||
**Independent Test**: Can be tested independently by creating products with different statuses (active, archived) and verifying that only active products appear on the landing page, or that archived products are clearly marked as not accepting feedback.
|
**Independent Test**: Can be tested independently by creating products with different statuses (active, archived) and verifying that only active products appear on the landing page (archived products are not displayed).
|
||||||
|
|
||||||
**Acceptance Scenarios**:
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
1. **Given** a product has status "active", **When** the landing page loads, **Then** the product appears in the list
|
1. **Given** a product has status "active", **When** the landing page loads, **Then** the product appears in the list
|
||||||
2. **Given** a product has status "archived", **When** the landing page loads, **Then** the product does NOT appear in the list (or appears with clear "not accepting feedback" indicator)
|
2. **Given** a product has status "archived", **When** the landing page loads, **Then** the product does NOT appear in the list
|
||||||
3. **Given** some products are active and some are archived, **When** the landing page loads, **Then** only active products are shown by default
|
3. **Given** some products are active and some are archived, **When** the landing page loads, **Then** only active products are shown
|
||||||
4. **Given** a visitor views the landing page, **When** they see a product listed, **Then** they can trust that clicking it will allow them to submit feedback
|
4. **Given** a visitor views the landing page, **When** they see a product listed, **Then** they can trust that clicking it will allow them to submit feedback
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -61,12 +71,12 @@ A visitor who already knows which product they want to submit feedback for can q
|
|||||||
### Edge Cases
|
### Edge Cases
|
||||||
|
|
||||||
- **What happens when there are no active products?** Display a message: "No products are currently accepting feedback. Please check back later."
|
- **What happens when there are no active products?** Display a message: "No products are currently accepting feedback. Please check back later."
|
||||||
- **What happens when all products are archived?** Same as no active products - show appropriate message explaining no products available
|
- **What happens when all products are archived?** Same as no active products - display the "No products are currently accepting feedback" message (archived products are not shown)
|
||||||
- **What happens when a product has no description?** Display product name only, or show placeholder text like "No description available"
|
- **What happens when a product has no description?** Display product name only (no description text or placeholder shown)
|
||||||
- **What happens when a visitor accesses the landing page while authenticated as a product owner?** They should still see the landing page (authentication doesn't affect anonymous submission access), or optionally redirect to dashboard
|
- **What happens when a visitor accesses the landing page while authenticated as a product owner?** They see the landing page normally (authentication doesn't affect landing page access)
|
||||||
- **What happens when product names are very long or contain special characters?** Ensure proper text truncation/wrapping and HTML escaping for XSS prevention
|
- **What happens when product names are very long or contain special characters?** Ensure proper text truncation/wrapping and HTML escaping for XSS prevention
|
||||||
- **What happens when multiple products have similar names?** Display them all clearly - let descriptions help differentiate
|
- **What happens when multiple products have identical names?** Display them all; sort by product_id alphabetically as secondary sort (after name)
|
||||||
- **What happens if a product's submission_url_slug is missing or invalid?** Skip that product in the listing with error logged, or show error state
|
- **What happens if a product's submission_url_slug is missing or invalid?** Skip that product silently in the listing and log an error for admin investigation
|
||||||
|
|
||||||
## Requirements *(mandatory)*
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
@@ -77,15 +87,16 @@ A visitor who already knows which product they want to submit feedback for can q
|
|||||||
- **FR-003**: System MUST filter products to show ONLY those with `status: active` in their config.yaml
|
- **FR-003**: System MUST filter products to show ONLY those with `status: active` in their config.yaml
|
||||||
- **FR-004**: System MUST display for each product: product name (`name` field from config.yaml)
|
- **FR-004**: System MUST display for each product: product name (`name` field from config.yaml)
|
||||||
- **FR-005**: System MUST provide a clickable link/button for each product that navigates to `/submit/{submission_url_slug}`
|
- **FR-005**: System MUST provide a clickable link/button for each product that navigates to `/submit/{submission_url_slug}`
|
||||||
- **FR-006**: System MUST handle products without descriptions gracefully (show name only or placeholder)
|
- **FR-006**: System MUST handle products without descriptions gracefully (show product name only, no placeholder text)
|
||||||
- **FR-007**: System MUST maintain existing direct URL functionality (`/submit/{product-slug}` continues to work)
|
- **FR-007**: System MUST maintain existing direct URL functionality (`/submit/{product-slug}` continues to work)
|
||||||
- **FR-008**: Landing page MUST be accessible to anonymous users (no authentication required)
|
- **FR-008**: Landing page MUST be accessible to anonymous users (no authentication required); authenticated users also see the landing page normally
|
||||||
- **FR-009**: System MUST sort products in a consistent, predictable order (alphabetical by name recommended)
|
- **FR-009**: System MUST sort products alphabetically by name (case-insensitive); if names are identical, use product_id alphabetically as secondary sort
|
||||||
- **FR-010**: System MUST handle the case where no active products exist (display appropriate message)
|
- **FR-010**: System MUST handle the case where no active products exist (display appropriate message)
|
||||||
- **FR-011**: Product listing MUST be server-side rendered (consistent with project's no-JavaScript requirement)
|
- **FR-011**: Product listing MUST be server-side rendered (consistent with project's no-JavaScript requirement)
|
||||||
- **FR-012**: System MUST escape all product names and descriptions to prevent XSS attacks
|
- **FR-012**: System MUST escape all product names and descriptions to prevent XSS attacks
|
||||||
- **FR-013**: Landing page MUST use the same minimal HTML/CSS styling as the rest of the application (no frameworks)
|
- **FR-013**: Landing page MUST use the same minimal HTML/CSS styling as the rest of the application (no frameworks)
|
||||||
- **FR-014**: System MUST log when the landing page is accessed (for monitoring/analytics)
|
- **FR-014**: System MUST log when the landing page is accessed (for monitoring/analytics)
|
||||||
|
- **FR-015**: System MUST skip products with missing or invalid `submission_url_slug` and log an error (product not shown to users)
|
||||||
|
|
||||||
### Key Entities
|
### Key Entities
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user