{% endblock %}
```
**Rule:** Use the specific nested block (`admin_content`, `page_content`) not the outer `content` block.
**Verify:** `grep -l "{% block content %}" app/templates/admin/*.html` should only return base.html.
### SQLAlchemy ORDER BY - Columns Only
SQLAlchemy `order_by()` requires actual database columns, not Python `@property`:
```python
class User(Base):
first_name = Column(String) # Database column ✓
last_name = Column(String) # Database column ✓
@property
def display_name(self): # Computed property ✗
return f"{self.first_name} {self.last_name}"
# WRONG - property is not a column
users = db.query(User).order_by(User.display_name).all()
# RIGHT - use actual columns
users = db.query(User).order_by(User.first_name, User.last_name).all()
```
**Rule:** Only Column-defined attributes in `filter()`, `order_by()`, `group_by()`.
### Service Function Parameters
Always match exact parameter names when calling service functions:
```python
# Service signature
async def send_email(
to_email: str,
subject: str,
html_content: str, # <-- Actual name
) -> bool:
# WRONG
await send_email(to_email="x", subject="y", body="z") # 'body' doesn't exist!
# RIGHT
await send_email(to_email="x", subject="y", html_content="z")
```
**Prevention:** Check function signature before calling; use IDE autocomplete.
### Email Service Database Settings (CRITICAL)
When calling `send_email()` from notification functions, MUST pass `db=db` to use database-configured SMTP settings:
```python
# WRONG - falls back to environment variables (often empty)
success, _ = await send_email(to_email, subject, html_content)
# RIGHT - uses database-configured SMTP settings
success, _ = await send_email(to_email, subject, html_content, db=db)
```
**Why this is insidious:** Test emails work (admin panel passes db), but notification emails fail silently (missing db). Hard to debug because SMTP "looks configured."
**Rule:** Every `send_email()` call in notification functions must include `db=db`.
### Database Migration for New Model Columns
When adding new columns to SQLAlchemy models, existing databases need ALTER TABLE:
```python
# After adding to models.py:
subscribe_all_requests = Column(Boolean, default=False, nullable=False)
# Run this to add column to existing database:
import sqlite3
conn = sqlite3.connect('instance/app.db')
cursor = conn.cursor()
cursor.execute('ALTER TABLE users ADD COLUMN subscribe_all_requests BOOLEAN DEFAULT 0 NOT NULL')
conn.commit()
conn.close()
```
**Symptom:** `sqlite3.OperationalError: no such column: users.new_column`
**Prevention:** After adding model columns, always run migration before testing.
### Recipient-Aware Email Links
Notification emails should link staff to admin pages, public users to public portal:
```python
def is_staff_user(db: Session, email: str) -> bool:
"""Check if email belongs to active staff user."""
from app.models import User
user = db.query(User).filter(
User.email == email.lower(),
User.is_active == True
).first()
return user is not None
def get_request_url_for_recipient(db, request, email: str) -> str:
"""Return admin URL for staff, public URL for others."""
if is_staff_user(db, email):
return f"{settings.BASE_URL}/admin/requests/{request.id}"
return f"{settings.BASE_URL}/requests/view/{request.request_number}"
```
**Usage:** Build email content per-recipient, not once for all:
```python
# WRONG - same link for everyone
html = f'View'
for email in recipients:
send_email(email, subject, html, db=db)
# RIGHT - customized link per recipient
for email in recipients:
url = get_request_url_for_recipient(db, request, email)
html = f'View'
send_email(email, subject, html, db=db)
```
### Dark Mode CSS (Bootstrap 5.3)
Use `bg-body-secondary` instead of `bg-light` for dark mode support:
```html
Content
Content
```
**Rule:** Replace all `bg-light` with `bg-body-secondary` in templates.
### View/Edit Page Card Consistency
View and Edit pages for the same entity should have consistent card ordering:
1. **Common cards first** (appear on both pages)
2. **Page-specific cards after** (only on View or Edit)
```
View Page: Edit Page:
├── Submitter Info ├── Submitter Info (common)
├── Email Notifications ├── Email Notifications (common)
├── Vendor Assignment ├── [form fields] (edit-specific)
├── Quick Actions
└── Status History (view-specific)
```
**Rule:** When adding cards, maintain ordering parity between View and Edit templates.
### Verify Exact Names (Universal Principle)
**Always verify exact names before using them.** This applies to:
| Context | Common Mistakes | Prevention |
|---------|-----------------|------------|
| Model fields | `start_datetime` vs `start_time` | Check models.py |
| Service params | `body` vs `html_content` | Check function signature |
| Template vars | `request` vs `maint_request` | Check route context |
| Property vs Column | `display_name` vs `first_name` | Check if @property or Column |
| Audit log fields | `details` vs `new_value` | Check AuditLog model |
**Rule:** When you assume a name, you're often wrong. Take 5 seconds to verify.
```python
# Before writing this:
booking.start_datetime # Are you SURE it's not start_time?
send_email(body=...) # Are you SURE it's not html_content?
User.display_name # Are you SURE it's a Column, not @property?
```
### Claude.md for Claude Code Handoff
Maintain a `Claude.md` file in the application root to facilitate switching to Claude Code for implementation, testing, debugging, and extension.
```markdown
# Project: [App Name]
## Quick Start
cd [project-dir] && source venv/bin/activate && python -m uvicorn app.main:app --reload
## Architecture
- Framework: FastAPI + SQLAlchemy 2.0 + Jinja2
- Auth: OAuth-only (Google), first-user=admin
- Database: SQLite (instance/app.db)
## Key Files
- app/models.py - All SQLAlchemy models
- app/config.py - Pydantic Settings
- app/templates_config.py - Jinja2 setup (ONE file)
- app/auth.py - OAuth + session management
## Current State
- [x] Core CRUD for [Module]
- [ ] Email notifications
- [ ] Reporting
## Known Issues
- Hairpin NAT requires hosts file entry for internal access
## Testing
pytest tests/ -v
pytest tests/test_[module].py -v -k "test_name"
## Critical Patterns
- Route ordering: /new before /{id}
- Template blocks: use admin_content, not content
- Never name domain objects "request"
```
**When to update Claude.md:**
- After completing a feature
- When discovering issues/workarounds
- Before ending a session
- When patterns change
**Benefits:**
- Claude Code starts with full context
- No re-explaining architecture decisions
- Known issues don't get re-discovered
- Testing commands ready to use
---
## Pre-Delivery Checklist (50 Items)
### Files Must Exist
- [ ] INSTALL-AND-RUN.bat
- [ ] UPDATE.bat
- [ ] scripts/run.bat
- [ ] scripts/run-tests.bat
- [ ] scripts/backup.bat
- [ ] scripts/config_crypto.py
- [ ] README.md
- [ ] INSTALL.md
- [ ] CHANGELOG.md
- [ ] .env.example
- [ ] pytest.ini
- [ ] tests/conftest.py
### Files Must NOT Exist in Package
- [ ] instance/app.db
- [ ] .env (only .env.example)
- [ ] venv/
- [ ] __pycache__/
### Code Patterns (grep checks)
- [ ] All .bat files have `cd /d "%~dp0"` or `cd /d "%APP_DIR%"`
- [ ] No hardcoded "session_token" (use SESSION_COOKIE_NAME)
- [ ] Only ONE file has `Jinja2Templates(`
- [ ] Only auth.py has password hashing
- [ ] Admin scripts have `net session` elevation check
### Authentication Checks (if OAuth enabled)
- [ ] Login page has no password form (OAuth-only)
- [ ] OAuth state cookie ≠ auth session cookie
- [ ] First user gets ADMIN role automatically
- [ ] GOOGLE_ALLOWED_DOMAIN is set in .env.example
### HTTPS/Production Checks (if applicable)
- [ ] BASE_URL uses `https://` for production
- [ ] HTTPS BASE_URL has no port number
- [ ] Response filename includes app name
- [ ] Cookies have `secure=True` when HTTPS
### Route/Template Checks
- [ ] Route ordering: `/new` before `/{id}` in all route files
- [ ] Template auth functions registered in `templates.env.globals`
- [ ] All `url_for()` routes have explicit `name=` parameter
- [ ] No domain objects named `request` (use `maint_request`, `booking`, etc.)
- [ ] Admin templates use `{% block admin_content %}` not `{% block content %}`
- [ ] ORDER BY clauses use Column fields, not @property attributes
### Navigation Checks
- [ ] All new features have navigation links to reach them
- [ ] Settings sub-features have links in appropriate settings tab
- [ ] CLAUDE.md Navigation Registry is current
- [ ] Navigation tests pass: `pytest tests/test_navigation.py -v`
- [ ] No "hidden features" (route + template exists but no UI link)
### Security Checks
- [ ] No `|safe` filter on user-generated content
- [ ] All POST/PUT/DELETE routes have authentication checks
- [ ] No secrets in code (grep for API_KEY, PASSWORD, SECRET)
- [ ] All POST forms have CSRF tokens
- [ ] File uploads validate extension AND MIME type
- [ ] No raw SQL queries with user input
- [ ] Sensitive settings use encryption (SENSITIVE_SETTINGS list)
- [ ] .env.example documents all required env vars
- [ ] No production secrets in .env.example
- [ ] Config values match between code defaults and .env.example
### Name Verification (verify against source)
- [ ] Model field names match models.py exactly
- [ ] Service function calls use exact parameter names
- [ ] Template variable names don't collide with reserved names
### Documentation
- [ ] Claude.md exists in project root (for Claude Code handoff)
- [ ] Claude.md has current state, known issues, critical patterns
---
## Quick Audits (Essential Checks)
### Architecture Audit
```bash
# Models should not have business methods
grep -n "def " app/models.py | grep -v "__\|property"
# Routes should not query database directly (minimize)
grep -rn "db.query\|\.filter\|\.all()" app/routes/
# Services should exist
ls app/services/
```
### Template Audit
```bash
# Single Jinja2Templates instance
grep -r "Jinja2Templates(" app/ --include="*.py"
# Should only show templates_config.py
# No hardcoded options
grep -r "