## Phase 4: Web Interface Implementation Implemented complete web interface with public and admin routes, templates, CSS, and development authentication. ### Core Features **Public Routes**: - Homepage with recent published notes - Note permalinks with microformats2 - Server-side rendering (Jinja2) **Admin Routes**: - Login via IndieLogin - Dashboard with note management - Create, edit, delete notes - Protected with @require_auth decorator **Development Authentication**: - Dev login bypass for local testing (DEV_MODE only) - Security safeguards per ADR-011 - Returns 404 when disabled **Templates & Frontend**: - Base layouts (public + admin) - 8 HTML templates with microformats2 - Custom responsive CSS (114 lines) - Error pages (404, 500) ### Bugfixes (v0.5.1 → v0.5.2) 1. **Cookie collision fix (v0.5.1)**: - Renamed auth cookie from "session" to "starpunk_session" - Fixed redirect loop between dev login and admin dashboard - Flask's session cookie no longer conflicts with auth 2. **HTTP 404 error handling (v0.5.1)**: - Update route now returns 404 for nonexistent notes - Delete route now returns 404 for nonexistent notes - Follows ADR-012 HTTP Error Handling Policy - Pattern consistency across all admin routes 3. **Note model enhancement (v0.5.2)**: - Exposed deleted_at field from database schema - Enables soft deletion verification in tests - Follows ADR-013 transparency principle ### Architecture **New ADRs**: - ADR-011: Development Authentication Mechanism - ADR-012: HTTP Error Handling Policy - ADR-013: Expose deleted_at Field in Note Model **Standards Compliance**: - Uses uv for Python environment - Black formatted, Flake8 clean - Follows git branching strategy - Version incremented per versioning strategy ### Test Results - 405/406 tests passing (99.75%) - 87% code coverage - All security tests passing - Manual testing confirmed working ### Documentation - Complete implementation reports in docs/reports/ - Architecture reviews in docs/reviews/ - Design documents in docs/design/ - CHANGELOG updated for v0.5.2 ### Files Changed **New Modules**: - starpunk/dev_auth.py - starpunk/routes/ (public, admin, auth, dev_auth) **Templates**: 10 files (base, pages, admin, errors) **Static**: CSS and optional JavaScript **Tests**: 4 test files for routes and templates **Docs**: 20+ architectural and implementation documents 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
9.8 KiB
ADR-012: HTTP Error Handling Policy
Status
Accepted
Context
During Phase 4 (Web Interface) implementation, a test failure revealed inconsistent error handling between GET and POST routes when accessing nonexistent resources:
GET /admin/edit/99999returns HTTP 404 (correct)POST /admin/edit/99999returns HTTP 302 redirect (incorrect)
This inconsistency creates several problems:
- Semantic confusion: HTTP 404 means "Not Found", but we were redirecting instead
- API incompatibility: Future Micropub API implementation requires proper HTTP status codes
- Debugging difficulty: Hard to distinguish between "note doesn't exist" and "operation failed"
- Test suite inconsistency: Tests expect 404, implementation returns 302
Traditional Web App Pattern
Many traditional web applications use:
- 404 for GET: Can't render a form for nonexistent resource
- 302 redirect for POST: Show user-friendly error message via flash
This provides good UX but sacrifices HTTP semantic correctness.
REST/API Pattern
REST APIs consistently use:
- 404 for all operations on nonexistent resources
- Applies to GET, POST, PUT, DELETE, etc.
This provides semantic correctness and API compatibility.
StarPunk's Requirements
- Human-facing web interface (Phase 4)
- Future Micropub API endpoint (Phase 5)
- Single-user system (simpler error handling needs)
- Standards compliance (IndieWeb specs require proper HTTP)
Decision
StarPunk will use REST-style error handling for all routes, returning HTTP 404 for any operation on a nonexistent resource, regardless of HTTP method.
Specific Rules
- All routes MUST return 404 when the target resource does not exist
- All routes SHOULD check resource existence before processing the request
- 404 responses MAY include user-friendly flash messages for web routes
- 404 responses MAY redirect to a safe location (e.g., dashboard) while still returning 404 status
Implementation Pattern
@bp.route("/operation/<int:resource_id>", methods=["GET", "POST"])
@require_auth
def operation(resource_id: int):
# 1. CHECK EXISTENCE FIRST
resource = get_resource(id=resource_id)
if not resource:
flash("Resource not found", "error")
return redirect(url_for("admin.dashboard")), 404 # ← MUST return 404
# 2. VALIDATE INPUT (for POST/PUT)
# ...
# 3. PERFORM OPERATION
# ...
# 4. RETURN SUCCESS
# ...
Status Code Policy
| Scenario | Status Code | Response Type | Flash Message |
|---|---|---|---|
| Resource not found | 404 | Redirect to dashboard | "Resource not found" |
| Validation failed | 302 | Redirect to form | "Invalid data: {details}" |
| Operation succeeded | 302 | Redirect to dashboard | "Success: {details}" |
| System error | 500 | Error page | "System error occurred" |
| Unauthorized | 302 | Redirect to login | "Authentication required" |
Flask Pattern for 404 with Redirect
Flask allows returning a tuple (response, status_code):
return redirect(url_for("admin.dashboard")), 404
This sends:
- HTTP 404 status code
- Location header pointing to dashboard
- Flash message in session
The client receives 404 but can follow the redirect to see the error message.
Rationale
Why REST-Style Over Web-Form-Style?
- Future API Compatibility: Micropub (Phase 5) requires proper HTTP semantics
- Standards Compliance: IndieWeb specs expect REST-like behavior
- Semantic Correctness: 404 means "not found" - this is universally understood
- Consistency: Simpler mental model - all operations follow same rules
- Debugging: Clear distinction between error types
- Test Intent: Test suite already expects this behavior
UX Considerations
Concern: Won't users see ugly error pages?
Mitigation:
- Flash messages provide context ("Note not found")
- 404 response includes redirect to dashboard
- Can implement custom 404 error handler with navigation
- Single-user system = developer is the user (understands HTTP)
Comparison to Delete Operation
The delete_note() function is idempotent - it succeeds even if the note doesn't exist. This is correct for delete operations (common REST pattern). However, the route should still check existence and return 404 for consistency:
- Idempotent implementation: Good (delete succeeds either way)
- Explicit existence check in route: Better (clear 404 for user)
Consequences
Positive
- Consistent behavior across all routes (GET, POST, DELETE)
- API-ready: Micropub implementation will work correctly
- Standards compliance: Meets IndieWeb/REST expectations
- Better testing: Status codes clearly indicate error types
- Clearer debugging: Know immediately if resource doesn't exist
- Simpler code: One pattern to follow everywhere
Negative
- Requires existence checks: Every route must check before operating
- Slight performance cost: Extra database query per request (minimal for SQLite)
- Different from some web apps: Traditional web apps often use redirects for all POST errors
Neutral
- Custom 404 handler needed: For good UX (but we'd want this anyway)
- Test suite updates: Some tests may need adjustment (but most already expect 404)
- Documentation: Need to document this pattern (but good practice anyway)
Implementation Checklist
Immediate (Phase 4 Fix)
- Fix
POST /admin/edit/<id>to return 404 for nonexistent notes - Verify
GET /admin/edit/<id>still returns 404 (already correct) - Update
POST /admin/delete/<id>to return 404 (optional, but recommended) - Update test
test_delete_nonexistent_note_shows_errorif delete route changed
Future (Phase 4 Completion)
- Create custom 404 error handler with navigation
- Document pattern in
/home/phil/Projects/starpunk/docs/standards/http-error-handling.md - Review all routes for consistency
- Add error handling section to coding standards
Phase 5 (Micropub API)
- Verify Micropub routes follow this pattern
- Return JSON error responses for API routes
- Maintain 404 status codes for missing resources
Examples
Good Example: Edit Note Form (GET)
@bp.route("/edit/<int:note_id>", methods=["GET"])
@require_auth
def edit_note_form(note_id: int):
note = get_note(id=note_id)
if not note:
flash("Note not found", "error")
return redirect(url_for("admin.dashboard")), 404 # ✓ CORRECT
return render_template("admin/edit.html", note=note)
Status: Currently implemented correctly
Bad Example: Update Note (POST) - Before Fix
@bp.route("/edit/<int:note_id>", methods=["POST"])
@require_auth
def update_note_submit(note_id: int):
# ✗ NO EXISTENCE CHECK
try:
note = update_note(id=note_id, content=content, published=published)
# ...
except Exception as e:
flash(f"Error: {e}", "error")
return redirect(url_for("admin.edit_note_form", note_id=note_id)) # ✗ Returns 302
Problem: Returns 302 redirect, not 404
Good Example: Update Note (POST) - After Fix
@bp.route("/edit/<int:note_id>", methods=["POST"])
@require_auth
def update_note_submit(note_id: int):
# ✓ CHECK EXISTENCE FIRST
existing_note = get_note(id=note_id, load_content=False)
if not existing_note:
flash("Note not found", "error")
return redirect(url_for("admin.dashboard")), 404 # ✓ CORRECT
# Process the update
# ...
Status: Needs implementation
References
- Test failure:
test_update_nonexistent_note_404intests/test_routes_admin.py:386 - Architectural review:
/home/phil/Projects/starpunk/docs/reviews/error-handling-rest-vs-web-patterns.md - RFC 7231 Section 6.5.4 (404 Not Found): https://tools.ietf.org/html/rfc7231#section-6.5.4
- IndieWeb Micropub spec: https://micropub.spec.indieweb.org/
- Flask documentation on status codes: https://flask.palletsprojects.com/en/latest/quickstart/#about-responses
Alternatives Considered
Alternative 1: Web-Form Pattern (Redirect All POST Errors)
Rejected because:
- Breaks semantic HTTP (404 means "not found")
- Incompatible with future Micropub API
- Makes debugging harder (can't distinguish error types by status code)
- Test suite already expects 404
Alternative 2: Hybrid Approach (404 for API, 302 for Web)
Rejected because:
- Adds complexity (need to detect context)
- Inconsistent behavior confuses developers
- Same route may serve both web and API clients
- Flask blueprint structure makes this awkward
Alternative 3: Exception-Based (Let Exceptions Propagate to Error Handler)
Rejected because:
- Less explicit (harder to understand flow)
- Error handlers are global (less flexibility per route)
- Flash messages harder to customize per route
- Lose ability to redirect to different locations per route
Notes
Performance Consideration
The existence check adds one database query per request:
existing_note = get_note(id=note_id, load_content=False) # SELECT query
With load_content=False, this is just a metadata query (no file I/O):
- SQLite query: ~0.1ms for indexed lookup
- Negligible overhead for single-user system
- Could be optimized later if needed (caching, etc.)
Future Enhancement: Error Handler
Custom 404 error handler can improve UX:
@app.errorhandler(404)
def not_found(error):
"""Custom 404 page with navigation"""
# Check if there's a flash message (from routes)
# Render custom template with link to dashboard
# Or redirect to dashboard for admin routes
return render_template('errors/404.html'), 404
This is optional but recommended for Phase 4 completion.
Revision History
- 2025-11-18: Initial decision (v0.4.0 development)
- Status: Accepted
- Supersedes: None
- Related: ADR-003 (Frontend Technology), Phase 4 Design