Phase 1: skeleton
This commit is contained in:
commit
51289ef456
18 changed files with 532 additions and 0 deletions
0
.env
Normal file
0
.env
Normal file
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
1
.python-version
Normal file
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
venv
|
||||||
531
CLAUDE.md
Normal file
531
CLAUDE.md
Normal file
|
|
@ -0,0 +1,531 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file is project memory for Claude Code. Drop it in the repo root, `cd` into
|
||||||
|
the project, run `claude`, and Claude Code will read it automatically. Work
|
||||||
|
through the phases below **in order**, one at a time — commit after each phase.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
A multi-feature Flask app built with the **app-factory + blueprint** pattern.
|
||||||
|
Every feature (auth, main, and anything you add later) lives in its own
|
||||||
|
blueprint folder, so new features drop in without touching existing code.
|
||||||
|
Ships with a full authentication flow (register / login / logout) and a
|
||||||
|
public index page.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- Python 3.11+ (pyenv-managed)
|
||||||
|
- Flask 3.x — app factory pattern
|
||||||
|
- Flask-SQLAlchemy — ORM, SQLite by default
|
||||||
|
- Flask-Login — session-based auth
|
||||||
|
- Flask-WTF + WTForms — forms, CSRF protection
|
||||||
|
- Flask-Migrate — Alembic migrations
|
||||||
|
- python-dotenv — env config
|
||||||
|
- Jinja2 templates, Bootstrap 5 via CDN (swap for your own CI later)
|
||||||
|
|
||||||
|
## Target Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
myapp/
|
||||||
|
├── app/
|
||||||
|
│ ├── __init__.py # app factory
|
||||||
|
│ ├── models.py # shared models (User)
|
||||||
|
│ ├── auth/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── forms.py
|
||||||
|
│ │ └── routes.py
|
||||||
|
│ └── main/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── routes.py
|
||||||
|
├── templates/
|
||||||
|
│ ├── base.html
|
||||||
|
│ ├── index.html
|
||||||
|
│ └── auth/
|
||||||
|
│ ├── login.html
|
||||||
|
│ └── register.html
|
||||||
|
├── static/
|
||||||
|
│ └── css/
|
||||||
|
├── config.py
|
||||||
|
├── run.py
|
||||||
|
├── requirements.txt
|
||||||
|
├── .env
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions for Claude Code
|
||||||
|
|
||||||
|
- One blueprint per feature, under `app/<feature>/`.
|
||||||
|
- Each blueprint: `__init__.py`, `routes.py`, and `forms.py` / `models.py` only if that feature needs them.
|
||||||
|
- Templates mirror blueprint structure: feature-specific templates go in `templates/<feature>/`.
|
||||||
|
- Every template extends `templates/base.html`.
|
||||||
|
- No secrets hardcoded — config comes from environment variables via `.env`.
|
||||||
|
- Any model change → `flask db migrate` then `flask db upgrade`, never edit the DB by hand.
|
||||||
|
- When asked to "implement Phase N", only touch the files listed for that phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Session Status (resume point)
|
||||||
|
|
||||||
|
Phase 0 is mostly done:
|
||||||
|
- `pyenv install 3.13 -s`, `pyenv local 3.13`, `pyenv virtualenv 3.13 venv`, and `git init` all completed.
|
||||||
|
- `.python-version` is set to `venv`.
|
||||||
|
- **Known issue:** inside the Claude Code sandboxed Bash tool, `$PATH` did not include `~/.pyenv/shims`, so the venv never actually activated in that tool — `python -V` kept resolving to the system Python.
|
||||||
|
- **Resolution in progress:** the user is activating the virtualenv manually in their own terminal (`pyenv activate venv` or equivalent), then quitting and restarting `claude` from inside that activated shell so the tool inherits the correct environment.
|
||||||
|
- **On resume:** verify with `which python` / `python -V` that the venv (Python 3.13, path containing `.pyenv/versions/3.13.14/envs/venv`) is active before running any `pip install` or `flask` commands. Once confirmed, proceed to Phase 1 (Skeleton).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build Tutorial
|
||||||
|
|
||||||
|
### Phase 0 — Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyenv install 3.13 -s
|
||||||
|
pyenv local 3.13
|
||||||
|
pyenv virtualenv 3.13 venv
|
||||||
|
pyenv activate venv
|
||||||
|
git init
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 1 — Skeleton
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p app/auth app/main templates/auth static/css
|
||||||
|
touch app/__init__.py app/models.py \
|
||||||
|
app/auth/__init__.py app/auth/routes.py app/auth/forms.py \
|
||||||
|
app/main/__init__.py app/main/routes.py \
|
||||||
|
templates/base.html templates/index.html \
|
||||||
|
templates/auth/login.html templates/auth/register.html \
|
||||||
|
config.py run.py requirements.txt .env .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2 — Dependencies
|
||||||
|
|
||||||
|
`requirements.txt`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Flask==3.0.3
|
||||||
|
Flask-SQLAlchemy==3.1.1
|
||||||
|
Flask-Login==0.6.3
|
||||||
|
Flask-WTF==1.2.1
|
||||||
|
Flask-Migrate==4.0.7
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
email-validator==2.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
FLASK_APP=run.py
|
||||||
|
FLASK_DEBUG=1
|
||||||
|
SECRET_KEY=change-me-to-a-random-string
|
||||||
|
DATABASE_URL=sqlite:///app.db
|
||||||
|
```
|
||||||
|
|
||||||
|
`.gitignore`:
|
||||||
|
|
||||||
|
```
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
instance/
|
||||||
|
.env
|
||||||
|
app.db
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prompt for Claude Code:**
|
||||||
|
```
|
||||||
|
claude "Implement Phase 2 exactly as specified in CLAUDE.md: create requirements.txt, .env, and .gitignore with the given content."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3 — Config & App Factory
|
||||||
|
|
||||||
|
`config.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-key-not-safe")
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db")
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
```
|
||||||
|
|
||||||
|
`app/__init__.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_login import LoginManager
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
login_manager = LoginManager()
|
||||||
|
migrate = Migrate()
|
||||||
|
|
||||||
|
def create_app(config_class=Config):
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(config_class)
|
||||||
|
|
||||||
|
db.init_app(app)
|
||||||
|
login_manager.init_app(app)
|
||||||
|
migrate.init_app(app, db)
|
||||||
|
|
||||||
|
login_manager.login_view = "auth.login"
|
||||||
|
login_manager.login_message_category = "info"
|
||||||
|
|
||||||
|
from app.auth.routes import auth_bp
|
||||||
|
from app.main.routes import main_bp
|
||||||
|
app.register_blueprint(auth_bp, url_prefix="/auth")
|
||||||
|
app.register_blueprint(main_bp)
|
||||||
|
|
||||||
|
from app import models # noqa: F401 — registers models with SQLAlchemy
|
||||||
|
|
||||||
|
return app
|
||||||
|
```
|
||||||
|
|
||||||
|
`run.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app import create_app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4 — User Model
|
||||||
|
|
||||||
|
`app/models.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datetime import datetime
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from app import db, login_manager
|
||||||
|
|
||||||
|
class User(UserMixin, db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(64), unique=True, nullable=False)
|
||||||
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<User {self.username}>"
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
return User.query.get(int(user_id))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5 — Auth Blueprint
|
||||||
|
|
||||||
|
`app/auth/forms.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||||
|
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
class RegistrationForm(FlaskForm):
|
||||||
|
username = StringField("Username", validators=[DataRequired(), Length(min=3, max=64)])
|
||||||
|
email = StringField("Email", validators=[DataRequired(), Email()])
|
||||||
|
password = PasswordField("Password", validators=[DataRequired(), Length(min=8)])
|
||||||
|
confirm_password = PasswordField(
|
||||||
|
"Confirm Password", validators=[DataRequired(), EqualTo("password")]
|
||||||
|
)
|
||||||
|
submit = SubmitField("Create Account")
|
||||||
|
|
||||||
|
def validate_username(self, username):
|
||||||
|
if User.query.filter_by(username=username.data).first():
|
||||||
|
raise ValidationError("That username is already taken.")
|
||||||
|
|
||||||
|
def validate_email(self, email):
|
||||||
|
if User.query.filter_by(email=email.data).first():
|
||||||
|
raise ValidationError("That email is already registered.")
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
email = StringField("Email", validators=[DataRequired(), Email()])
|
||||||
|
password = PasswordField("Password", validators=[DataRequired()])
|
||||||
|
remember_me = BooleanField("Remember Me")
|
||||||
|
submit = SubmitField("Log In")
|
||||||
|
```
|
||||||
|
|
||||||
|
`app/auth/routes.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
|
from app import db
|
||||||
|
from app.models import User
|
||||||
|
from app.auth.forms import RegistrationForm, LoginForm
|
||||||
|
|
||||||
|
auth_bp = Blueprint("auth", __name__, template_folder="../../templates/auth")
|
||||||
|
|
||||||
|
@auth_bp.route("/register", methods=["GET", "POST"])
|
||||||
|
def register():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
form = RegistrationForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User(username=form.username.data, email=form.email.data)
|
||||||
|
user.set_password(form.password.data)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Account created. You can log in now.", "success")
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
return render_template("auth/register.html", form=form)
|
||||||
|
|
||||||
|
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
form = LoginForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User.query.filter_by(email=form.email.data).first()
|
||||||
|
if user is None or not user.check_password(form.password.data):
|
||||||
|
flash("Invalid email or password.", "danger")
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
login_user(user, remember=form.remember_me.data)
|
||||||
|
next_page = request.args.get("next")
|
||||||
|
if not next_page or urlparse(next_page).netloc != "":
|
||||||
|
next_page = url_for("main.index")
|
||||||
|
return redirect(next_page)
|
||||||
|
return render_template("auth/login.html", form=form)
|
||||||
|
|
||||||
|
@auth_bp.route("/logout")
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
flash("You have been logged out.", "info")
|
||||||
|
return redirect(url_for("main.index"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 6 — Main Blueprint
|
||||||
|
|
||||||
|
`app/main/routes.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
main_bp = Blueprint("main", __name__)
|
||||||
|
|
||||||
|
@main_bp.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 7 — Templates
|
||||||
|
|
||||||
|
`templates/base.html`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}MyApp{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{{ url_for('main.index') }}">MyApp</a>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<span class="navbar-text">Hi, {{ current_user.username }}</span>
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('auth.logout') }}">Log out</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('auth.login') }}">Log in</a>
|
||||||
|
<a class="btn btn-primary btn-sm" href="{{ url_for('auth.register') }}">Register</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
`templates/index.html`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Home{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Welcome{% if current_user.is_authenticated %}, {{ current_user.username }}{% endif %}!</h1>
|
||||||
|
{% if not current_user.is_authenticated %}
|
||||||
|
<p>Please <a href="{{ url_for('auth.login') }}">log in</a> or
|
||||||
|
<a href="{{ url_for('auth.register') }}">create an account</a>.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
`templates/auth/login.html`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Log In{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Log In</h2>
|
||||||
|
<form method="POST" novalidate class="col-md-6">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.email.label(class="form-label") }}
|
||||||
|
{{ form.email(class="form-control") }}
|
||||||
|
{% for error in form.email.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.password.label(class="form-label") }}
|
||||||
|
{{ form.password(class="form-control") }}
|
||||||
|
{% for error in form.password.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
{{ form.remember_me(class="form-check-input") }}
|
||||||
|
{{ form.remember_me.label(class="form-check-label") }}
|
||||||
|
</div>
|
||||||
|
{{ form.submit(class="btn btn-primary") }}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
`templates/auth/register.html`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Register{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Create Account</h2>
|
||||||
|
<form method="POST" novalidate class="col-md-6">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.username.label(class="form-label") }}
|
||||||
|
{{ form.username(class="form-control") }}
|
||||||
|
{% for error in form.username.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.email.label(class="form-label") }}
|
||||||
|
{{ form.email(class="form-control") }}
|
||||||
|
{% for error in form.email.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.password.label(class="form-label") }}
|
||||||
|
{{ form.password(class="form-control") }}
|
||||||
|
{% for error in form.password.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.confirm_password.label(class="form-label") }}
|
||||||
|
{{ form.confirm_password(class="form-control") }}
|
||||||
|
{% for error in form.confirm_password.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{{ form.submit(class="btn btn-primary") }}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 8 — Wire Up & Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flask db init
|
||||||
|
flask db migrate -m "initial: User model"
|
||||||
|
flask db upgrade
|
||||||
|
flask run
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit `http://127.0.0.1:5000/` → should show the index page.
|
||||||
|
Visit `/auth/register` → create an account → redirected to `/auth/login`.
|
||||||
|
Log in → index page now greets you by username and shows "Log out".
|
||||||
|
|
||||||
|
### Phase 9 — Adding a New Feature (this is the "multiple feature" part)
|
||||||
|
|
||||||
|
Every additional feature follows the same pattern as `auth`/`main`. Example —
|
||||||
|
adding a `dashboard` feature that only logged-in users can see:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p app/dashboard templates/dashboard
|
||||||
|
touch app/dashboard/__init__.py app/dashboard/routes.py templates/dashboard/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
`app/dashboard/routes.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask import Blueprint, render_template
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
dashboard_bp = Blueprint("dashboard", __name__, template_folder="../../templates/dashboard")
|
||||||
|
|
||||||
|
@dashboard_bp.route("/dashboard")
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
return render_template("dashboard/index.html")
|
||||||
|
```
|
||||||
|
|
||||||
|
Register it in `app/__init__.py` alongside the other blueprints:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.dashboard.routes import dashboard_bp
|
||||||
|
app.register_blueprint(dashboard_bp)
|
||||||
|
```
|
||||||
|
|
||||||
|
Repeat this pattern for every new feature — that's the whole point of the
|
||||||
|
blueprint layout: no existing file changes when a feature is added, only new
|
||||||
|
ones.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using This File With the Claude Code CLI
|
||||||
|
|
||||||
|
1. Place `CLAUDE.md` at the project root before running `claude`.
|
||||||
|
2. Work phase by phase, e.g.:
|
||||||
|
```
|
||||||
|
claude "Implement Phase 3 exactly as specified in CLAUDE.md"
|
||||||
|
```
|
||||||
|
3. Review the diff Claude Code produces, run/test it, `git commit`.
|
||||||
|
4. Move to the next phase. Don't skip ahead — later phases assume earlier files exist exactly as specified.
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Can register a new user
|
||||||
|
- [ ] Duplicate email/username is rejected with a clear error
|
||||||
|
- [ ] Password is hashed, never stored in plain text
|
||||||
|
- [ ] Can log in and log out
|
||||||
|
- [ ] Index page differs for logged-in vs. anonymous users
|
||||||
|
- [ ] CSRF token present in both forms (`form.hidden_tag()`)
|
||||||
|
- [ ] `flask db upgrade` runs clean on a fresh clone
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- Forgetting to set `SECRET_KEY` / `FLASK_APP` in `.env`.
|
||||||
|
- Circular imports between `app/__init__.py` and blueprints — always import blueprints *inside* `create_app()`, not at module top level.
|
||||||
|
- Forgetting `login_manager.login_view = "auth.login"` → `@login_required` redirects to a 404 instead of the login page.
|
||||||
|
- Running the app before `flask db upgrade` → "no such table: user".
|
||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
0
app/auth/__init__.py
Normal file
0
app/auth/__init__.py
Normal file
0
app/auth/forms.py
Normal file
0
app/auth/forms.py
Normal file
0
app/auth/routes.py
Normal file
0
app/auth/routes.py
Normal file
0
app/main/__init__.py
Normal file
0
app/main/__init__.py
Normal file
0
app/main/routes.py
Normal file
0
app/main/routes.py
Normal file
0
app/models.py
Normal file
0
app/models.py
Normal file
0
config.py
Normal file
0
config.py
Normal file
0
requirements.txt
Normal file
0
requirements.txt
Normal file
0
run.py
Normal file
0
run.py
Normal file
0
templates/auth/login.html
Normal file
0
templates/auth/login.html
Normal file
0
templates/auth/register.html
Normal file
0
templates/auth/register.html
Normal file
0
templates/base.html
Normal file
0
templates/base.html
Normal file
0
templates/index.html
Normal file
0
templates/index.html
Normal file
Loading…
Add table
Reference in a new issue