## 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>
12 KiB
Delete Route Implementation Specification
Date: 2025-11-18
Component: Admin Routes - Delete Note
File: /home/phil/Projects/starpunk/starpunk/routes/admin.py
Function: delete_note_submit() (lines 173-206)
ADR: ADR-012 (HTTP Error Handling Policy)
Implementation Requirements
Objective
Modify the delete route to check note existence before deletion and return HTTP 404 with an error message when attempting to delete a nonexistent note.
Exact Code Change
Current Implementation (Lines 173-206)
@bp.route("/delete/<int:note_id>", methods=["POST"])
@require_auth
def delete_note_submit(note_id: int):
"""
Handle note deletion
Deletes a note after confirmation.
Requires authentication.
Args:
note_id: Database ID of note to delete
Form data:
confirm: Must be 'yes' to proceed with deletion
Returns:
Redirect to dashboard with success/error message
Decorator: @require_auth
"""
# Check for confirmation
if request.form.get("confirm") != "yes":
flash("Deletion cancelled", "info")
return redirect(url_for("admin.dashboard"))
try:
delete_note(id=note_id, soft=False)
flash("Note deleted successfully", "success")
except ValueError as e:
flash(f"Error deleting note: {e}", "error")
except Exception as e:
flash(f"Unexpected error deleting note: {e}", "error")
return redirect(url_for("admin.dashboard"))
Required Implementation (Lines 173-206)
@bp.route("/delete/<int:note_id>", methods=["POST"])
@require_auth
def delete_note_submit(note_id: int):
"""
Handle note deletion
Deletes a note after confirmation.
Requires authentication.
Args:
note_id: Database ID of note to delete
Form data:
confirm: Must be 'yes' to proceed with deletion
Returns:
Redirect to dashboard with success/error message
Decorator: @require_auth
"""
# Check if note exists first (per ADR-012)
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
# Check for confirmation
if request.form.get("confirm") != "yes":
flash("Deletion cancelled", "info")
return redirect(url_for("admin.dashboard"))
try:
delete_note(id=note_id, soft=False)
flash("Note deleted successfully", "success")
except ValueError as e:
flash(f"Error deleting note: {e}", "error")
except Exception as e:
flash(f"Unexpected error deleting note: {e}", "error")
return redirect(url_for("admin.dashboard"))
Line-by-Line Changes
Insert After Line 192 (after docstring, before confirmation check)
Add these 4 lines:
# Check if note exists first (per ADR-012)
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
Result: Lines shift down by 5 (including blank line)
No Other Changes Required
- Docstring: No changes
- Confirmation check: No changes (shifts to line 199)
- Deletion logic: No changes (shifts to line 203)
- Return statement: No changes (shifts to line 211)
Implementation Details
Function Call: get_note(id=note_id, load_content=False)
Purpose: Check if note exists in database
Parameters:
id=note_id: Look up by database ID (primary key)load_content=False: Metadata only (no file I/O)
Returns:
Noteobject if foundNoneif not found or soft-deleted
Performance: ~0.1ms (single SELECT query)
Flash Message: "Note not found"
Purpose: User-facing error message
Category: "error" (red alert in UI)
Why this wording:
- Consistent with edit route (line 151)
- Simple and clear
- Test checks for "not found" substring
- ADR-012 example uses this exact message
Return Statement: return redirect(url_for("admin.dashboard")), 404
Purpose: Return HTTP 404 with redirect
Flask Pattern: Tuple (response, status_code)
- First element: Response object (redirect)
- Second element: HTTP status code (404)
Result:
- HTTP 404 status sent to client
- Location header:
/admin/ - Flash message stored in session
- Client can follow redirect to see error
Why not just return 404 error page:
- Better UX (user sees dashboard with error, not blank 404 page)
- Consistent with update route pattern
- Per ADR-012: "404 responses MAY redirect to a safe location"
Validation Checklist
Before Implementing
- Read ADR-012 (HTTP Error Handling Policy)
- Review similar implementation in
update_note_submit()(line 148-152) - Verify
get_noteis imported (line 15 - already imported ✅) - Verify test expectations in
test_delete_nonexistent_note_shows_error
After Implementing
- Code follows exact pattern from update route
- Existence check happens BEFORE confirmation check
- Flash message is "Note not found" with category "error"
- Return statement includes 404 status code
- No other logic changed
- Imports unchanged (get_note already imported)
Testing
- Run failing test:
uv run pytest tests/test_routes_admin.py::TestAdminDeleteRoutes::test_delete_nonexistent_note_shows_error -v - Verify test now passes
- Run all delete route tests:
uv run pytest tests/test_routes_admin.py::TestAdminDeleteRoutes -v - Verify all tests still pass (no regressions)
- Run full admin route tests:
uv run pytest tests/test_routes_admin.py -v - Verify 406/406 tests pass
Expected Test Results
Before Fix
FAILED tests/test_routes_admin.py::TestAdminDeleteRoutes::test_delete_nonexistent_note_shows_error
AssertionError: assert False
+ where False = (b'error' in b'...deleted successfully...' or b'not found' in b'...')
After Fix
PASSED tests/test_routes_admin.py::TestAdminDeleteRoutes::test_delete_nonexistent_note_shows_error
Edge Cases Handled
Case 1: Note Exists
Scenario: User deletes existing note Behavior:
- Existence check passes (note found)
- Confirmation check (if confirmed, proceed)
- Deletion succeeds
- Flash: "Note deleted successfully"
- Return: 302 redirect
Test Coverage: test_delete_redirects_to_dashboard
Case 2: Note Doesn't Exist
Scenario: User deletes nonexistent note (ID 99999) Behavior:
- Existence check fails (note not found)
- Flash: "Note not found"
- Return: 404 with redirect (no deletion attempted)
Test Coverage: test_delete_nonexistent_note_shows_error ← This fix
Case 3: Note Soft-Deleted
Scenario: User deletes note that was already soft-deleted Behavior:
get_note()excludes soft-deleted notes (WHERE deleted_at IS NULL)- Existence check fails (note not found from user perspective)
- Flash: "Note not found"
- Return: 404 with redirect
Test Coverage: Covered by get_note() behavior (implicit)
Case 4: Deletion Not Confirmed
Scenario: User submits form without confirm=yes
Behavior:
- Existence check passes (note found)
- Confirmation check fails
- Flash: "Deletion cancelled"
- Return: 302 redirect (no deletion, no 404)
Test Coverage: Existing tests (no change)
Performance Impact
Database Queries
Before:
- DELETE query (inside delete_note)
After:
- SELECT query (get_note - existence check)
- DELETE query (inside delete_note)
Overhead: ~0.1ms per deletion request
Why This is Acceptable
- Single-user system (not high traffic)
- Deletions are rare operations
- Correctness > performance for edge cases
- Consistent with update route (already accepts this overhead)
load_content=Falseavoids file I/O (only metadata query)
Consistency with Other Routes
Edit Routes (Already Implemented)
GET /admin/edit/ (line 118-122):
note = get_note(id=note_id)
if not note:
flash("Note not found", "error")
return redirect(url_for("admin.dashboard")), 404
POST /admin/edit/ (line 148-152):
# Check if note exists 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
Delete Route (This Implementation)
POST /admin/delete/ (new lines 193-197):
# Check if note exists first (per ADR-012)
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
Pattern Consistency: ✅ 100% identical to update route
ADR-012 Compliance
Required Elements
| Requirement | Status |
|---|---|
| Return 404 for nonexistent resource | ✅ Yes (return ..., 404) |
| Check existence before operation | ✅ Yes (get_note() before delete_note()) |
| Include user-friendly flash message | ✅ Yes (flash("Note not found", "error")) |
| Redirect to safe location | ✅ Yes (redirect(url_for("admin.dashboard"))) |
Implementation Pattern (ADR-012, lines 56-74)
Spec 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
# ...
Our Implementation: ✅ Follows pattern exactly
Common Implementation Mistakes to Avoid
Mistake 1: Check Existence After Confirmation
Wrong:
# Check for confirmation
if request.form.get("confirm") != "yes":
# ...
# Check if note exists ← TOO LATE
existing_note = get_note(id=note_id, load_content=False)
Why Wrong: User confirms deletion of nonexistent note, then gets 404
Correct: Check existence FIRST (before any user interaction)
Mistake 2: Forget load_content=False
Wrong:
existing_note = get_note(id=note_id) # Loads file content
Why Wrong: Unnecessary file I/O (we only need to check existence)
Correct: get_note(id=note_id, load_content=False) (metadata only)
Mistake 3: Return 302 Instead of 404
Wrong:
if not existing_note:
flash("Note not found", "error")
return redirect(url_for("admin.dashboard")) # ← Missing 404
Why Wrong: Returns HTTP 302 (redirect), not 404 (not found)
Correct: return redirect(...), 404 (tuple with status code)
Mistake 4: Wrong Flash Message Category
Wrong:
flash("Note not found", "info") # ← Should be "error"
Why Wrong: Not an error in UI (blue alert, not red)
Correct: flash("Note not found", "error") (red error alert)
Mistake 5: Catching NoteNotFoundError from delete_note()
Wrong:
try:
delete_note(id=note_id, soft=False)
except NoteNotFoundError: # ← delete_note doesn't raise this
flash("Note not found", "error")
return redirect(...), 404
Why Wrong:
delete_note()is idempotent (doesn't raise on missing note)- Existence check should happen BEFORE calling delete_note
- Violates separation of concerns (route layer vs data layer)
Correct: Explicit existence check before deletion (as specified)
Final Verification
Code Review Checklist
- Existence check is first operation (after docstring)
- Uses
get_note(id=note_id, load_content=False)exactly - Flash message is
"Note not found"with category"error" - Return statement is
return redirect(url_for("admin.dashboard")), 404 - No changes to confirmation logic
- No changes to deletion logic
- No changes to exception handling
- No changes to imports
- Code matches update route pattern exactly
Test Validation
- Run specific test: Should PASS
- Run delete route tests: All should PASS
- Run admin route tests: All should PASS (406/406)
- Run full test suite: All should PASS
Documentation
- This implementation spec reviewed
- Root cause analysis document reviewed
- ADR-012 referenced and understood
- Changes documented in changelog
- Version incremented per versioning strategy
Summary
Change: Add 4 lines (existence check + error handling) Location: After line 192, before confirmation check Impact: 1 test changes from FAIL to PASS Result: 406/406 tests passing ✅
This is the minimal, correct implementation that complies with ADR-012 and maintains consistency with existing routes.