1033 lines
15 KiB
Markdown
1033 lines
15 KiB
Markdown
# Would You Rather Website - Project Context
|
|
|
|
## Project Goal
|
|
|
|
Build a lightweight "Would You Rather" website hosted on a Raspberry Pi 5.
|
|
|
|
Users visit the website, answer a daily question, and immediately see aggregated results after voting.
|
|
|
|
Example:
|
|
|
|
> Would you rather be blind or deaf?
|
|
|
|
* Blind
|
|
* Deaf
|
|
|
|
After voting:
|
|
|
|
* Blind: 62%
|
|
* Deaf: 38%
|
|
|
|
Total votes: 523
|
|
|
|
---
|
|
|
|
# Current Development Strategy
|
|
|
|
Development is being done on a laptop first.
|
|
|
|
Reasons:
|
|
|
|
* Easier coding workflow
|
|
* Faster testing
|
|
* Git integration from day one
|
|
* Easier version control
|
|
* Safer than developing directly on the Raspberry Pi
|
|
|
|
Deployment strategy:
|
|
|
|
```text
|
|
Laptop
|
|
↓
|
|
Git
|
|
↓
|
|
GitHub
|
|
↓
|
|
Raspberry Pi
|
|
↓
|
|
git pull
|
|
↓
|
|
Production
|
|
```
|
|
|
|
---
|
|
|
|
# Technology Stack
|
|
|
|
## Backend
|
|
|
|
* Python
|
|
* Flask
|
|
* Flask application factory pattern
|
|
* Flask blueprints for public and admin routes
|
|
|
|
## Database
|
|
|
|
* SQLite
|
|
* Local development database path: `instance/questions.db`
|
|
|
|
## Frontend
|
|
|
|
* HTML
|
|
* CSS
|
|
* Jinja2 templates
|
|
|
|
## Authentication
|
|
|
|
* Flask sessions
|
|
* Werkzeug password hashing for admin passwords
|
|
|
|
## Version Control
|
|
|
|
* Git
|
|
* GitHub
|
|
|
|
## Future Production
|
|
|
|
* Gunicorn
|
|
* Systemd
|
|
* Nginx
|
|
* HTTPS with Let's Encrypt
|
|
|
|
---
|
|
|
|
# Current Local Project Structure
|
|
|
|
The project has been refactored out of a single-file Flask app.
|
|
|
|
Current structure:
|
|
|
|
```text
|
|
would-you-rather/
|
|
│
|
|
├── .venv/
|
|
├── .gitignore
|
|
├── app.py
|
|
├── run.py
|
|
├── requirements.txt
|
|
├── project-context.md
|
|
│
|
|
├── app/
|
|
│ ├── __init__.py
|
|
│ ├── admin.py
|
|
│ ├── database.py
|
|
│ ├── routes.py
|
|
│ │
|
|
│ ├── templates/
|
|
│ │ ├── public/
|
|
│ │ │ ├── index.html
|
|
│ │ │ └── results.html
|
|
│ │ │
|
|
│ │ └── admin/
|
|
│ │ ├── dashboard.html
|
|
│ │ ├── delete_confirm.html
|
|
│ │ ├── login.html
|
|
│ │ └── question_form.html
|
|
│ │
|
|
│ └── static/
|
|
│ └── style.css
|
|
│
|
|
├── scripts/
|
|
│ └── init_db.py
|
|
│
|
|
├── tests/
|
|
│ └── test_app.py
|
|
│
|
|
└── instance/
|
|
└── questions.db
|
|
```
|
|
|
|
Notes:
|
|
|
|
* `app.py` remains as a compatibility entrypoint.
|
|
* `run.py` is the preferred local development entrypoint.
|
|
* `instance/` is ignored by Git.
|
|
* Root-level `questions.db` is ignored and is no longer the active app database.
|
|
|
|
---
|
|
|
|
# Current Application Architecture
|
|
|
|
## App Factory
|
|
|
|
File:
|
|
|
|
```text
|
|
app/__init__.py
|
|
```
|
|
|
|
Purpose:
|
|
|
|
* Creates the Flask app with `create_app()`.
|
|
* Configures `SECRET_KEY`.
|
|
* Configures SQLite database path.
|
|
* Ensures the `instance/` directory exists.
|
|
* Registers public and admin blueprints.
|
|
* Registers database teardown behavior.
|
|
|
|
Important config:
|
|
|
|
```python
|
|
SECRET_KEY = os.environ.get("WYR_SECRET_KEY", "dev-only-change-me")
|
|
DATABASE = instance/questions.db
|
|
```
|
|
|
|
Production must set a real `WYR_SECRET_KEY`.
|
|
|
|
---
|
|
|
|
## Database Helpers
|
|
|
|
File:
|
|
|
|
```text
|
|
app/database.py
|
|
```
|
|
|
|
Purpose:
|
|
|
|
* Opens SQLite connections with `get_db()`.
|
|
* Uses `sqlite3.Row` so routes/templates can access fields by name.
|
|
* Closes request-scoped connections with `close_db()`.
|
|
* Creates all tables with `init_db()`.
|
|
|
|
---
|
|
|
|
## Public Routes
|
|
|
|
File:
|
|
|
|
```text
|
|
app/routes.py
|
|
```
|
|
|
|
Routes:
|
|
|
|
```text
|
|
/
|
|
/vote
|
|
/results
|
|
```
|
|
|
|
Behavior:
|
|
|
|
* `/` displays the question scheduled for today's date.
|
|
* `/vote` accepts POSTed `qid` and `vote`.
|
|
* `/vote` only accepts votes for today's scheduled question.
|
|
* `/results` displays today's vote counts, percentages, and total votes.
|
|
* Empty states are shown if no question exists for today.
|
|
* Duplicate voting is still blocked by hashing the request IP address.
|
|
|
|
Current duplicate prevention is temporary.
|
|
|
|
Future plan:
|
|
|
|
* Use cookies or session identifiers instead of IP hashing.
|
|
|
|
---
|
|
|
|
# Current Database Design
|
|
|
|
## Questions Table
|
|
|
|
```sql
|
|
CREATE TABLE questions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
question TEXT NOT NULL,
|
|
option_a TEXT NOT NULL,
|
|
option_b TEXT NOT NULL,
|
|
date TEXT NOT NULL UNIQUE
|
|
);
|
|
```
|
|
|
|
Purpose:
|
|
|
|
Stores scheduled daily questions.
|
|
|
|
---
|
|
|
|
## Votes Table
|
|
|
|
```sql
|
|
CREATE TABLE votes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
question_id INTEGER,
|
|
vote TEXT NOT NULL,
|
|
voter_hash TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
Purpose:
|
|
|
|
Stores user votes.
|
|
|
|
Stored values:
|
|
|
|
* `question_id`
|
|
* selected vote, currently `A` or `B`
|
|
* `voter_hash`
|
|
|
|
---
|
|
|
|
## Admins Table
|
|
|
|
```sql
|
|
CREATE TABLE admins (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
Purpose:
|
|
|
|
Stores admin login accounts.
|
|
|
|
Passwords must never be stored in plain text.
|
|
|
|
Current implementation uses:
|
|
|
|
```python
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
```
|
|
|
|
---
|
|
|
|
# Admin Dashboard V1
|
|
|
|
Admin functionality has been implemented.
|
|
|
|
File:
|
|
|
|
```text
|
|
app/admin.py
|
|
```
|
|
|
|
Routes:
|
|
|
|
```text
|
|
/admin/login
|
|
/admin/logout
|
|
/admin/account
|
|
/admin/dashboard
|
|
/admin/question/new
|
|
/admin/question/edit/<id>
|
|
/admin/question/delete/<id>
|
|
```
|
|
|
|
Implemented behavior:
|
|
|
|
* Admin login with username/password.
|
|
* Password verification with Werkzeug password hashing.
|
|
* Successful login stores `session["admin"] = True`.
|
|
* Protected admin pages use `@admin_required`.
|
|
* Unauthenticated users are redirected to `/admin/login`.
|
|
* Admins can change their username and password from `/admin/account`.
|
|
* Changing credentials requires the current password.
|
|
* Dashboard lists all questions ordered by date descending.
|
|
* Admins can create questions.
|
|
* Admins can edit questions.
|
|
* Admins can delete questions.
|
|
* Delete also removes votes for that question.
|
|
* Duplicate question dates are blocked.
|
|
* Required fields are validated.
|
|
|
|
Dashboard V1 is intentionally simple.
|
|
|
|
No JavaScript calendar is implemented yet.
|
|
|
|
---
|
|
|
|
# Local Development Setup
|
|
|
|
## Install Dependencies
|
|
|
|
Use the virtual environment:
|
|
|
|
```bash
|
|
source .venv/bin/activate
|
|
pip install -r requirements.txt
|
|
```
|
|
|
|
Or run commands directly through `.venv/bin/python`.
|
|
|
|
---
|
|
|
|
## Initialize the Database
|
|
|
|
Run:
|
|
|
|
```bash
|
|
.venv/bin/python scripts/init_db.py
|
|
```
|
|
|
|
This creates:
|
|
|
|
```text
|
|
instance/questions.db
|
|
```
|
|
|
|
It also seeds:
|
|
|
|
* one sample question
|
|
* one local admin account
|
|
|
|
Default local admin login:
|
|
|
|
```text
|
|
username: admin
|
|
password: admin
|
|
```
|
|
|
|
To override the seeded admin account:
|
|
|
|
```bash
|
|
WYR_ADMIN_USERNAME=myname WYR_ADMIN_PASSWORD=mypassword .venv/bin/python scripts/init_db.py
|
|
```
|
|
|
|
If the admin user already exists, the script will not overwrite that password.
|
|
|
|
---
|
|
|
|
## Run the Site Locally
|
|
|
|
Preferred command:
|
|
|
|
```bash
|
|
.venv/bin/python run.py
|
|
```
|
|
|
|
Then open:
|
|
|
|
```text
|
|
http://127.0.0.1:5000
|
|
```
|
|
|
|
Admin login:
|
|
|
|
```text
|
|
http://127.0.0.1:5000/admin/login
|
|
```
|
|
|
|
Default local login:
|
|
|
|
```text
|
|
admin / admin
|
|
```
|
|
|
|
---
|
|
|
|
## Restart After Local Changes
|
|
|
|
If the app is running manually in a terminal:
|
|
|
|
1. Stop it with `Ctrl+C`.
|
|
2. Make changes or pull changes from Git.
|
|
3. Restart the app.
|
|
|
|
Local restart command:
|
|
|
|
```bash
|
|
.venv/bin/python run.py
|
|
```
|
|
|
|
If dependencies changed:
|
|
|
|
```bash
|
|
.venv/bin/pip install -r requirements.txt
|
|
```
|
|
|
|
If database tables changed:
|
|
|
|
```bash
|
|
.venv/bin/python scripts/init_db.py
|
|
```
|
|
|
|
Typical local update flow:
|
|
|
|
```bash
|
|
git pull
|
|
.venv/bin/pip install -r requirements.txt
|
|
.venv/bin/python scripts/init_db.py
|
|
.venv/bin/python run.py
|
|
```
|
|
|
|
---
|
|
|
|
## Run Tests
|
|
|
|
Run:
|
|
|
|
```bash
|
|
.venv/bin/python -m unittest discover -s tests
|
|
```
|
|
|
|
Current automated tests cover:
|
|
|
|
* public empty state
|
|
* voting flow
|
|
* duplicate vote prevention
|
|
* admin login success
|
|
* admin login failure
|
|
* admin credential changes
|
|
* protected dashboard redirect
|
|
* duplicate date validation when creating questions
|
|
|
|
---
|
|
|
|
# Manual Testing Checklist
|
|
|
|
## Public Site
|
|
|
|
1. Start the app:
|
|
|
|
```bash
|
|
.venv/bin/python run.py
|
|
```
|
|
|
|
2. Open:
|
|
|
|
```text
|
|
http://127.0.0.1:5000
|
|
```
|
|
|
|
3. Confirm the sample question appears if it is scheduled for today's date.
|
|
|
|
4. Click one answer.
|
|
|
|
5. Confirm the site redirects to `/results`.
|
|
|
|
6. Confirm results show percentages and total votes.
|
|
|
|
7. Vote again from the same browser/IP.
|
|
|
|
8. Confirm the duplicate vote does not increase the vote count.
|
|
|
|
---
|
|
|
|
## Admin Site
|
|
|
|
1. Open:
|
|
|
|
```text
|
|
http://127.0.0.1:5000/admin/login
|
|
```
|
|
|
|
2. Log in with:
|
|
|
|
```text
|
|
admin / admin
|
|
```
|
|
|
|
3. Confirm the dashboard appears.
|
|
|
|
4. Click `Change Credentials`.
|
|
|
|
5. Confirm changing credentials requires the current password.
|
|
|
|
6. Save new credentials, log out, and confirm the new login works.
|
|
|
|
7. Click `Create Question`.
|
|
|
|
8. Add today's date or a future date, question, option A, and option B.
|
|
|
|
9. Save and confirm the new question appears in the dashboard.
|
|
|
|
10. Edit the question and confirm changes persist.
|
|
|
|
11. Try creating another question with the same date.
|
|
|
|
12. Confirm this error appears:
|
|
|
|
```text
|
|
Date already has a question.
|
|
```
|
|
|
|
13. Delete a question and confirm it disappears from the dashboard.
|
|
|
|
14. Log out.
|
|
|
|
15. Visit `/admin/dashboard` while logged out and confirm it redirects to `/admin/login`.
|
|
|
|
---
|
|
|
|
# Development Notes
|
|
|
|
## Current Compatibility Entrypoints
|
|
|
|
Preferred local command:
|
|
|
|
```bash
|
|
.venv/bin/python run.py
|
|
```
|
|
|
|
Compatibility command:
|
|
|
|
```bash
|
|
.venv/bin/python app.py
|
|
```
|
|
|
|
Both create the app through:
|
|
|
|
```python
|
|
from app import create_app
|
|
```
|
|
|
|
---
|
|
|
|
## Git Ignore Rules
|
|
|
|
The following local-only files/directories are ignored:
|
|
|
|
```text
|
|
.venv/
|
|
__pycache__/
|
|
*.pyc
|
|
questions.db
|
|
instance/
|
|
.env
|
|
.vscode/
|
|
.idea/
|
|
```
|
|
|
|
The active SQLite database is intentionally not committed.
|
|
|
|
---
|
|
|
|
## What To Commit To Git
|
|
|
|
Safe and expected to commit:
|
|
|
|
```text
|
|
app/
|
|
scripts/
|
|
tests/
|
|
app.py
|
|
run.py
|
|
requirements.txt
|
|
.gitignore
|
|
project-context.md
|
|
```
|
|
|
|
Also commit deletions/moves when files are refactored.
|
|
|
|
Current old paths that were replaced:
|
|
|
|
```text
|
|
init_db.py
|
|
templates/
|
|
static/
|
|
```
|
|
|
|
These old paths should remain deleted once the new `app/` package layout is committed.
|
|
|
|
---
|
|
|
|
## What Not To Commit
|
|
|
|
Keep these local only:
|
|
|
|
```text
|
|
.venv/
|
|
instance/
|
|
instance/questions.db
|
|
questions.db
|
|
.env
|
|
__pycache__/
|
|
.vscode/
|
|
.idea/
|
|
```
|
|
|
|
Reasons:
|
|
|
|
* `.venv/` is machine-specific.
|
|
* SQLite databases contain local runtime data.
|
|
* `.env` may contain secrets.
|
|
* cache/editor files are not project source.
|
|
|
|
Even with a self-hosted private Git server, avoid committing secrets.
|
|
|
|
---
|
|
|
|
# Future Admin Features
|
|
|
|
## Dashboard Version 2
|
|
|
|
Use a proper calendar interface.
|
|
|
|
Possible library:
|
|
|
|
* FullCalendar
|
|
|
|
Desired behavior:
|
|
|
|
```text
|
|
June 2026
|
|
|
|
13 Question
|
|
14 Question
|
|
15 Empty
|
|
16 Question
|
|
```
|
|
|
|
Clicking a date:
|
|
|
|
* opens edit view if the date has a question
|
|
* opens create view if the date is empty
|
|
|
|
---
|
|
|
|
## Search
|
|
|
|
Search questions by:
|
|
|
|
* date
|
|
* question text
|
|
|
|
---
|
|
|
|
## Statistics View
|
|
|
|
Admin dashboard should eventually show:
|
|
|
|
```text
|
|
Question
|
|
|
|
Blind: 142
|
|
Deaf: 98
|
|
|
|
Total: 240
|
|
```
|
|
|
|
Useful for engagement analytics.
|
|
|
|
---
|
|
|
|
# Future Public Features
|
|
|
|
## Better Duplicate Vote Prevention
|
|
|
|
Replace IP hashing with:
|
|
|
|
* cookies
|
|
* sessions
|
|
* or lightweight anonymous visitor IDs
|
|
|
|
---
|
|
|
|
## Archive
|
|
|
|
Browse historical questions.
|
|
|
|
---
|
|
|
|
## Comments
|
|
|
|
Discussion under questions.
|
|
|
|
---
|
|
|
|
## Country Statistics
|
|
|
|
Example:
|
|
|
|
```text
|
|
Denmark
|
|
|
|
Blind: 65%
|
|
Deaf: 35%
|
|
|
|
United States
|
|
|
|
Blind: 52%
|
|
Deaf: 48%
|
|
```
|
|
|
|
---
|
|
|
|
## AI Generated Images
|
|
|
|
One image per question.
|
|
|
|
Example:
|
|
|
|
```text
|
|
Would you rather live underwater or in space?
|
|
```
|
|
|
|
with generated artwork.
|
|
|
|
---
|
|
|
|
## User Accounts
|
|
|
|
Optional.
|
|
|
|
Users can:
|
|
|
|
* save history
|
|
* track participation
|
|
|
|
---
|
|
|
|
# Future AI Features
|
|
|
|
Not a priority yet.
|
|
|
|
Planned later.
|
|
|
|
Possible route:
|
|
|
|
```text
|
|
Generate next 30 questions
|
|
```
|
|
|
|
Workflow:
|
|
|
|
1. AI generates questions.
|
|
2. Questions fill empty dates.
|
|
3. Admin reviews.
|
|
4. Admin approves.
|
|
|
|
Potential providers:
|
|
|
|
* OpenAI
|
|
* OpenRouter
|
|
* Mistral
|
|
|
|
---
|
|
|
|
# Future Raspberry Pi Deployment
|
|
|
|
Local Raspberry Pi testing is currently done manually on the home network.
|
|
|
|
The future goal is to run the app as a managed service with `systemd`, then later put it behind Gunicorn/Nginx for production.
|
|
|
|
---
|
|
|
|
## Raspberry Pi Local Network Test Setup
|
|
|
|
Install system dependencies:
|
|
|
|
```bash
|
|
sudo apt update
|
|
sudo apt install -y git python3 python3-venv
|
|
```
|
|
|
|
Clone the repo:
|
|
|
|
```bash
|
|
git clone <your-self-hosted-git-url> wyr
|
|
cd wyr
|
|
```
|
|
|
|
Create and install into a virtual environment:
|
|
|
|
```bash
|
|
python3 -m venv .venv
|
|
.venv/bin/pip install -r requirements.txt
|
|
```
|
|
|
|
Set local secrets and initialize the database:
|
|
|
|
```bash
|
|
export WYR_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_hex(32))')"
|
|
export WYR_ADMIN_USERNAME="youradmin"
|
|
export WYR_ADMIN_PASSWORD="yourstrongpassword"
|
|
|
|
.venv/bin/python scripts/init_db.py
|
|
```
|
|
|
|
Run on the home LAN:
|
|
|
|
```bash
|
|
WYR_SECRET_KEY="$WYR_SECRET_KEY" .venv/bin/flask --app run run --host 0.0.0.0 --port 5000
|
|
```
|
|
|
|
Open from another device on the same network:
|
|
|
|
```text
|
|
http://<raspberry-pi-ip>:5000
|
|
```
|
|
|
|
Admin:
|
|
|
|
```text
|
|
http://<raspberry-pi-ip>:5000/admin/login
|
|
```
|
|
|
|
Do not port-forward this app to the public internet yet.
|
|
|
|
---
|
|
|
|
## Restart On The Pi During Manual Testing
|
|
|
|
If running manually in a terminal:
|
|
|
|
1. Press `Ctrl+C`.
|
|
2. Pull changes.
|
|
3. Install any dependency updates.
|
|
4. Run database setup if needed.
|
|
5. Start the app again.
|
|
|
|
Command sequence:
|
|
|
|
```bash
|
|
git pull
|
|
.venv/bin/pip install -r requirements.txt
|
|
.venv/bin/python scripts/init_db.py
|
|
WYR_SECRET_KEY="$WYR_SECRET_KEY" .venv/bin/flask --app run run --host 0.0.0.0 --port 5000
|
|
```
|
|
|
|
If the Pi reboots, manually `cd` back into the project and run the Flask command again.
|
|
|
|
This will become easier after adding `systemd`.
|
|
|
|
---
|
|
|
|
## Future Systemd Service
|
|
|
|
Future goal:
|
|
|
|
```bash
|
|
sudo systemctl start wyr
|
|
sudo systemctl stop wyr
|
|
sudo systemctl restart wyr
|
|
sudo systemctl status wyr
|
|
```
|
|
|
|
Benefits:
|
|
|
|
* app can start automatically after Pi reboot
|
|
* easier restart command
|
|
* logs can be viewed with `journalctl`
|
|
* app can run in the background without an open terminal
|
|
|
|
Example future log command:
|
|
|
|
```bash
|
|
journalctl -u wyr -f
|
|
```
|
|
|
|
This is not implemented yet.
|
|
|
|
---
|
|
|
|
Planned deployment stack:
|
|
|
|
```text
|
|
Internet
|
|
↓
|
|
Cloudflare
|
|
↓
|
|
Nginx
|
|
↓
|
|
Gunicorn
|
|
↓
|
|
Flask
|
|
↓
|
|
SQLite
|
|
```
|
|
|
|
---
|
|
|
|
# Deployment Workflow
|
|
|
|
On laptop:
|
|
|
|
```bash
|
|
git add .
|
|
git commit -m "feature"
|
|
git push
|
|
```
|
|
|
|
On Pi:
|
|
|
|
```bash
|
|
git pull
|
|
.venv/bin/pip install -r requirements.txt
|
|
.venv/bin/python scripts/init_db.py
|
|
```
|
|
|
|
Then manually restart the Flask command.
|
|
|
|
Later, after the `systemd` service exists, the Pi restart step will become:
|
|
|
|
```bash
|
|
sudo systemctl restart wyr
|
|
```
|
|
|
|
---
|
|
|
|
# Immediate Roadmap
|
|
|
|
## Current Status
|
|
|
|
Local refactor and Admin Dashboard V1 are implemented.
|
|
|
|
---
|
|
|
|
## Next Step
|
|
|
|
Add cooler layout and styling:
|
|
|
|
* improve the public voting page design
|
|
* improve the results page design
|
|
* improve admin dashboard styling
|
|
* make buttons, forms, and navigation feel more polished
|
|
* add better spacing, typography, and responsive layout
|
|
|
|
---
|
|
|
|
## After Styling
|
|
|
|
Polish the admin flow and public behavior:
|
|
|
|
* improve layout
|
|
* add clearer success messages
|
|
* add better vote duplicate handling
|
|
* improve the empty state when no question is scheduled for today
|
|
* add CSRF protection before public internet exposure
|
|
|
|
---
|
|
|
|
## After Local Pi Testing
|
|
|
|
Implement:
|
|
|
|
* `systemd` service for easier Pi management
|
|
* automatic start on reboot
|
|
* clearer production environment variables
|
|
* basic log inspection workflow with `journalctl`
|
|
|
|
---
|
|
|
|
## After Admin Polish
|
|
|
|
Implement:
|
|
|
|
* calendar view
|
|
* question scheduling by date
|
|
* admin statistics
|
|
|
|
---
|
|
|
|
## After Calendar
|
|
|
|
Implement:
|
|
|
|
* AI-assisted question generation
|
|
|
|
---
|
|
|
|
# Long-Term Goal
|
|
|
|
A polished daily "Would You Rather" website that:
|
|
|
|
* runs on a Raspberry Pi 5
|
|
* uses a custom domain
|
|
* supports HTTPS
|
|
* allows browser-based question management
|
|
* supports AI-assisted content generation
|
|
* scales beyond the initial MVP
|