## 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>
34 KiB
Phase 4: Web Interface - Implementation Design
Date: 2025-11-18 Phase: 4 Status: Design Complete, Ready for Implementation Dependencies: Phase 3 (Authentication) complete Version: 0.5.0 target
Executive Summary
Phase 4 implements the complete web interface for StarPunk, including public routes for viewing notes and admin routes for managing content. This phase brings together all previous work (core utilities, data models, notes management, authentication) into a cohesive user-facing application.
Key Deliverables:
- Public interface (homepage, note permalinks)
- Admin interface (dashboard, note editor, login/logout)
- Development authentication mechanism
- Template system with microformats
- Basic CSS styling
- Optional JavaScript enhancements
Scope
In Scope for Phase 4
Public Routes:
- Homepage with recent notes
- Individual note permalinks
- Feed discovery (RSS link)
Admin Routes:
- Login page
- Admin dashboard (note list)
- Create note form
- Edit note form
- Delete note confirmation
- Logout
Authentication Integration:
- Production auth (IndieLogin)
- Development auth mechanism (new)
- Session management
- Protected route enforcement
Frontend:
- Jinja2 templates
- Custom CSS (no framework)
- Optional JavaScript (markdown preview)
- Microformats2 markup
Development Tools:
- Dev auth for local testing
- Configuration validation
- Development mode indicators
Out of Scope
Deferred to Future Phases:
- RSS feed generation (Phase 5)
- Micropub endpoint (Phase 6)
- API routes (Phase 7)
- Media uploads
- Advanced search
- Tags/categories
- Themes/customization
Architecture Overview
Component Diagram
┌─────────────────────────────────────────────────────────┐
│ Web Interface Layer │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Public Routes│ │ Admin Routes │ │
│ ├──────────────┤ ├──────────────┤ │
│ │ / │ │ /admin/login │ │
│ │ /note/<slug> │ │ /admin │ │
│ │ │ │ /admin/new │ │
│ │ │ │ /admin/edit │ │
│ └──────────────┘ └──────────────┘ │
│ │ │ │
│ │ │ │
│ ├──────────────────────────────┤ │
│ │ Template Rendering │ │
│ │ (Jinja2) │ │
│ └──────────────────────────────┘ │
│ │ │
└────────────────────────┼─────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────┐
│ Business Logic Layer │
├─────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐│
│ │ Notes Module │ │ Auth Module │ │ Dev Auth ││
│ │ (existing) │ │ (existing) │ │ (new) ││
│ └──────────────┘ └──────────────┘ └──────────────┘│
└─────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────┐
│ Data Layer │
├─────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Markdown │ │ SQLite DB │ │
│ │ Files │ │ (metadata) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
Request Flow
Public Note View:
User → GET /note/my-first-note
↓
Route Handler → get_note_by_slug('my-first-note')
↓
Notes Module → Query database for metadata
↓
Notes Module → Read markdown file
↓
Notes Module → Render markdown to HTML
↓
Template Render → note.html with microformats
↓
User ← HTML Response
Admin Note Creation:
User → GET /admin/new
↓
Auth Check → require_auth decorator
↓
Session Valid? → No → Redirect to /admin/login
→ Yes ↓
Route Handler → Render new.html form
↓
User ← HTML Form
User → POST /admin/new (markdown content)
↓
Auth Check → require_auth decorator
↓
Route Handler → create_note(content, published=True)
↓
Notes Module → Generate slug, write file, insert DB
↓
Route Handler → Redirect to /admin (dashboard)
↓
User ← Redirect with flash message
Routes Specification
Public Routes
GET /
Purpose: Homepage showing recent published notes
Handler: public.index()
Logic:
def index():
notes = get_all_notes(
published=True,
limit=20,
order_by='created_at DESC'
)
return render_template('index.html', notes=notes)
Template: templates/index.html
- List of notes (h-feed)
- Each note: title/content preview (h-entry)
- Pagination (if >20 notes)
- Link to RSS feed
Response:
- 200 OK with HTML
- Content-Type: text/html
- Microformats: h-feed with h-entry children
GET /note/<slug>
Purpose: Individual note permalink
Handler: public.note(slug)
Logic:
def note(slug):
note = get_note_by_slug(slug)
if not note or not note.published:
abort(404)
return render_template('note.html', note=note)
Template: templates/note.html
- Full note content (h-entry)
- Publication date
- Permalink URL
- Microformats markup
Response:
- 200 OK with HTML (note found)
- 404 Not Found (unpublished or missing)
- Content-Type: text/html
Microformats:
<article class="h-entry">
<div class="e-content">{{ note.html|safe }}</div>
<footer>
<a class="u-url" href="{{ url_for('public.note', slug=note.slug) }}">
<time class="dt-published" datetime="{{ note.created_at.isoformat() }}">
{{ note.created_at.strftime('%B %d, %Y') }}
</time>
</a>
</footer>
</article>
Admin Routes (Protected)
All admin routes require authentication via @require_auth decorator.
GET /admin/login
Purpose: Display login form
Handler: auth_routes.login_form()
Logic:
def login_form():
# If already logged in, redirect to dashboard
session_token = request.cookies.get('session')
if session_token and verify_session(session_token):
return redirect(url_for('admin.dashboard'))
return render_template('admin/login.html')
Template: templates/admin/login.html
- Form with "me" URL input
- Submit to POST /admin/login
- Link to IndieLogin documentation
- If DEV_MODE: show dev login link
Response: 200 OK with HTML
POST /admin/login
Purpose: Initiate IndieLogin authentication
Handler: auth_routes.login_initiate()
Logic:
def login_initiate():
me_url = request.form.get('me')
try:
auth_url = initiate_login(me_url)
return redirect(auth_url)
except ValueError as e:
flash(str(e), 'error')
return redirect(url_for('auth_routes.login_form'))
Response:
- 302 Redirect to IndieLogin.com
- Or 302 back to login form with error
GET /auth/callback
Purpose: Handle IndieLogin callback
Handler: auth_routes.callback()
Logic:
def callback():
code = request.args.get('code')
state = request.args.get('state')
try:
session_token = handle_callback(code, state)
response = redirect(url_for('admin.dashboard'))
response.set_cookie('session', session_token,
httponly=True, secure=True,
samesite='Lax', max_age=30*24*60*60)
flash('Login successful!', 'success')
return response
except (InvalidStateError, UnauthorizedError, IndieLoginError) as e:
flash(f'Login failed: {e}', 'error')
return redirect(url_for('auth_routes.login_form'))
Response:
- 302 Redirect to /admin with session cookie
- Or 302 to login with error
POST /admin/logout
Purpose: Destroy session and logout
Handler: auth_routes.logout()
Logic:
@require_auth
def logout():
session_token = request.cookies.get('session')
if session_token:
destroy_session(session_token)
response = redirect(url_for('public.index'))
response.delete_cookie('session')
flash('Logged out successfully', 'success')
return response
Response: 302 Redirect to homepage
GET /admin
Purpose: Admin dashboard with note list
Handler: admin.dashboard()
Protection: @require_auth
Logic:
@require_auth
def dashboard():
notes = get_all_notes(order_by='created_at DESC')
return render_template('admin/dashboard.html',
notes=notes,
user_me=g.user_me)
Template: templates/admin/dashboard.html
- List all notes (published and drafts)
- Each note: title, date, status, edit/delete buttons
- "New Note" button
- Logout link
- If DEV_MODE: warning banner
Response: 200 OK with HTML
GET /admin/new
Purpose: Create new note form
Handler: admin.new_note_form()
Protection: @require_auth
Logic:
@require_auth
def new_note_form():
return render_template('admin/new.html')
Template: templates/admin/new.html
- Markdown textarea
- Published checkbox (default: checked)
- Submit button
- Cancel link to dashboard
- Optional: JavaScript markdown preview
Response: 200 OK with HTML
POST /admin/new
Purpose: Create new note
Handler: admin.create_note_submit()
Protection: @require_auth
Logic:
@require_auth
def create_note_submit():
content = request.form.get('content')
published = 'published' in request.form
try:
note = create_note(content, published)
flash(f'Note created: {note.slug}', 'success')
return redirect(url_for('admin.dashboard'))
except ValueError as e:
flash(f'Error creating note: {e}', 'error')
return redirect(url_for('admin.new_note_form'))
Response:
- 302 Redirect to dashboard (success)
- 302 Redirect to form (error)
GET /admin/edit/<int:note_id>
Purpose: Edit note form
Handler: admin.edit_note_form(note_id)
Protection: @require_auth
Logic:
@require_auth
def edit_note_form(note_id):
note = get_note_by_id(note_id)
if not note:
abort(404)
return render_template('admin/edit.html', note=note)
Template: templates/admin/edit.html
- Pre-filled markdown textarea
- Published checkbox
- Save button
- Delete button (confirmation)
- Cancel link
Response:
- 200 OK with HTML (note found)
- 404 Not Found (note missing)
POST /admin/edit/<int:note_id>
Purpose: Update note
Handler: admin.update_note_submit(note_id)
Protection: @require_auth
Logic:
@require_auth
def update_note_submit(note_id):
content = request.form.get('content')
published = 'published' in request.form
try:
note = update_note(note_id, content, published)
flash(f'Note updated: {note.slug}', 'success')
return redirect(url_for('admin.dashboard'))
except ValueError as e:
flash(f'Error updating note: {e}', 'error')
return redirect(url_for('admin.edit_note_form', note_id=note_id))
Response:
- 302 Redirect to dashboard (success)
- 302 Redirect to form (error)
POST /admin/delete/<int:note_id>
Purpose: Delete note
Handler: admin.delete_note_submit(note_id)
Protection: @require_auth
Logic:
@require_auth
def delete_note_submit(note_id):
if request.form.get('confirm') != 'yes':
flash('Deletion cancelled', 'info')
return redirect(url_for('admin.dashboard'))
try:
delete_note(note_id)
flash('Note deleted', 'success')
except ValueError as e:
flash(f'Error deleting note: {e}', 'error')
return redirect(url_for('admin.dashboard'))
Response: 302 Redirect to dashboard
Development Routes (DEV_MODE only)
GET /dev/login
Purpose: Development mode instant login
Handler: dev_auth.dev_login()
Enabled: Only if DEV_MODE=true
Logic:
def dev_login():
if not current_app.config.get('DEV_MODE'):
abort(404) # Route doesn't exist in production
me = current_app.config.get('DEV_ADMIN_ME')
session_token = create_session(me)
current_app.logger.warning(
f"DEV MODE: Created session for {me} without authentication"
)
response = redirect(url_for('admin.dashboard'))
response.set_cookie('session', session_token,
httponly=True, secure=False,
samesite='Lax', max_age=30*24*60*60)
flash('DEV MODE: Logged in without authentication', 'warning')
return response
Response: 302 Redirect to /admin with session cookie
Security: Returns 404 if DEV_MODE disabled
Template Structure
Base Templates
templates/base.html
Purpose: Base layout for public pages
Structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}StarPunk{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="alternate" type="application/rss+xml" href="/feed.xml">
{% block head %}{% endblock %}
</head>
<body>
{% if config.DEV_MODE %}
<div class="dev-mode-warning">
⚠️ DEVELOPMENT MODE - Authentication bypassed
</div>
{% endif %}
<header>
<h1><a href="/">StarPunk</a></h1>
<nav>
<a href="/">Home</a>
<a href="/feed.xml">RSS</a>
</nav>
</header>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash flash-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer>
<p>StarPunk v{{ config.VERSION }}</p>
</footer>
</body>
</html>
templates/admin/base.html
Purpose: Base layout for admin pages
Extends: base.html
Additions:
- Admin navigation (Dashboard, New Note, Logout)
- User identity display
- Admin-specific styling
Public Templates
templates/index.html
Purpose: Homepage with note list
Microformats: h-feed containing h-entry items
Key Elements:
- Note list (latest 20)
- Each note: preview, date, permalink
- RSS feed link
- If empty: welcome message
templates/note.html
Purpose: Single note display
Microformats: h-entry
Key Elements:
- Full note content (rendered markdown)
- Publication date
- Permalink URL
- Back to home link
Admin Templates
templates/admin/login.html
Purpose: Login form
Key Elements:
- "me" URL input field
- Submit button
- Link to IndieLogin docs
- If DEV_MODE: "Quick Dev Login" link
templates/admin/dashboard.html
Purpose: Admin note list
Key Elements:
- Welcome message with user identity
- "New Note" button
- Table of all notes:
- Title/content preview
- Created date
- Status (published/draft)
- Edit button
- Delete button (with confirmation)
- Logout link
templates/admin/new.html
Purpose: Create note form
Key Elements:
- Markdown textarea
- Published checkbox (default checked)
- Character/word count (optional JS)
- Submit button
- Cancel link
- Optional: Live markdown preview (JS)
templates/admin/edit.html
Purpose: Edit note form
Similar to: new.html
Additional Elements:
- Pre-filled content
- Note metadata display (created date, slug)
- Delete button
- Update button
CSS Architecture
File: static/css/style.css
Philosophy:
- Mobile-first responsive design
- CSS custom properties for theming
- ~300-400 lines total
- No framework dependencies
- Semantic class names
Structure:
/* === Variables === */
:root {
/* Colors */
--color-text: #333;
--color-bg: #fff;
--color-link: #0066cc;
--color-border: #ddd;
--color-success: #28a745;
--color-error: #dc3545;
--color-warning: #ffc107;
/* Typography */
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'SF Mono', Monaco, 'Courier New', monospace;
--font-size-base: 16px;
--line-height: 1.6;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 2rem;
--spacing-xl: 4rem;
/* Layout */
--max-width: 42rem;
--border-radius: 4px;
}
/* === Reset & Base === */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
font-size: var(--font-size-base);
line-height: var(--line-height);
color: var(--color-text);
background: var(--color-bg);
padding: var(--spacing-md);
}
/* === Typography === */
h1, h2, h3 { margin-bottom: var(--spacing-md); }
p { margin-bottom: var(--spacing-md); }
a { color: var(--color-link); }
code { font-family: var(--font-mono); }
/* === Layout === */
header, main, footer {
max-width: var(--max-width);
margin: 0 auto;
}
header {
margin-bottom: var(--spacing-lg);
border-bottom: 1px solid var(--color-border);
}
/* === Components === */
.note-list { /* ... */ }
.h-entry { /* ... */ }
.flash { /* ... */ }
.button { /* ... */ }
.form-group { /* ... */ }
/* === Dev Mode Warning === */
.dev-mode-warning {
background: var(--color-error);
color: white;
padding: var(--spacing-sm);
text-align: center;
font-weight: bold;
}
/* === Admin === */
.admin-nav { /* ... */ }
.note-table { /* ... */ }
/* === Responsive === */
@media (min-width: 768px) {
body { padding: var(--spacing-lg); }
}
JavaScript Architecture
File: static/js/preview.js (Optional)
Purpose: Live markdown preview in admin editor
Implementation:
// Progressive enhancement - form works without this
(function() {
'use strict';
// Only run on note editor pages
const textarea = document.getElementById('note-content');
if (!textarea) return;
// Create preview container
const preview = document.createElement('div');
preview.id = 'markdown-preview';
preview.className = 'preview-panel';
textarea.parentNode.appendChild(preview);
// Use marked.js from CDN (loaded in template)
if (typeof marked === 'undefined') {
console.warn('Marked.js not loaded, preview disabled');
return;
}
// Update preview on input
let debounceTimer;
textarea.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
preview.innerHTML = marked.parse(textarea.value);
}, 300);
});
// Initial render
preview.innerHTML = marked.parse(textarea.value);
})();
Load in template (optional):
{% if request.path.startswith('/admin/') %}
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="{{ url_for('static', filename='js/preview.js') }}"></script>
{% endif %}
Graceful degradation: Form works without JavaScript
Development Authentication Implementation
See ADR-011 for complete specification.
Module: starpunk/dev_auth.py
Functions:
def dev_login() -> Response:
"""Create development session instantly"""
def is_dev_mode() -> bool:
"""Check if development mode enabled"""
def validate_dev_config() -> None:
"""Validate development configuration"""
Configuration Validation
def validate_config(app):
"""Validate app configuration on startup"""
dev_mode = app.config.get('DEV_MODE', False)
if dev_mode:
# Warn prominently
app.logger.warning(
"=" * 60 + "\n"
"WARNING: Development authentication enabled!\n"
"This should NEVER be used in production.\n"
"Set DEV_MODE=false for production deployments.\n" +
"=" * 60
)
# Require DEV_ADMIN_ME
if not app.config.get('DEV_ADMIN_ME'):
raise ConfigError("DEV_MODE requires DEV_ADMIN_ME")
# Register dev routes
register_dev_routes(app)
else:
# Production mode: require IndieLogin config
if not app.config.get('ADMIN_ME'):
raise ConfigError("Production mode requires ADMIN_ME")
Visual Indicators
<!-- All templates check for DEV_MODE -->
{% if config.DEV_MODE %}
<div class="dev-mode-warning">
⚠️ DEVELOPMENT MODE - Authentication bypassed
</div>
{% endif %}
File Organization
starpunk/
├── __init__.py
├── auth.py # Production auth (existing)
├── dev_auth.py # Development auth (new)
├── config.py # Configuration (update)
├── database.py # Database access (existing)
├── models.py # Data models (existing)
├── notes.py # Note management (existing)
├── utils.py # Utilities (existing)
└── routes/
├── __init__.py # Route registration
├── public.py # Public routes (new)
├── admin.py # Admin routes (new)
├── auth.py # Auth routes (new)
└── dev_auth.py # Dev auth routes (new, conditional)
templates/
├── base.html # Public base template
├── index.html # Homepage
├── note.html # Note permalink
├── admin/
│ ├── base.html # Admin base template
│ ├── login.html # Login form
│ ├── dashboard.html # Admin dashboard
│ ├── new.html # Create note
│ └── edit.html # Edit note
└── dev/
└── login.html # Dev login (optional)
static/
├── css/
│ └── style.css # Single stylesheet
└── js/
└── preview.js # Markdown preview (optional)
tests/
├── test_routes_public.py # Public route tests (new)
├── test_routes_admin.py # Admin route tests (new)
└── test_dev_auth.py # Dev auth tests (new)
Configuration Updates
Environment Variables
# Existing
SITE_URL=https://starpunk.example.com
ADMIN_ME=https://yoursite.com
SESSION_SECRET=<random-secret>
DATA_PATH=./data
# New for Phase 4
DEV_MODE=false # Enable development authentication
DEV_ADMIN_ME= # Identity for dev mode (required if DEV_MODE=true)
VERSION=0.5.0 # Application version
starpunk/config.py Updates
class Config:
# Existing config...
# Development mode (default: false)
DEV_MODE = os.getenv('DEV_MODE', 'false').lower() == 'true'
DEV_ADMIN_ME = os.getenv('DEV_ADMIN_ME', '')
# Application metadata
VERSION = os.getenv('VERSION', '0.5.0')
@staticmethod
def validate():
"""Validate configuration on startup"""
if Config.DEV_MODE:
if not Config.DEV_ADMIN_ME:
raise ValueError("DEV_MODE requires DEV_ADMIN_ME")
# Log warning
else:
if not Config.ADMIN_ME:
raise ValueError("Production mode requires ADMIN_ME")
Security Considerations
Authentication
- All admin routes protected by
@require_auth - Session validation on every request
- HttpOnly, Secure cookies in production
- Dev auth clearly separated and guarded
Input Validation
- Markdown content: no special validation (markdown is safe)
- Form inputs: Flask form validation
- Slugs: validated by notes module
- URLs: validated by auth module
CSRF Protection
- State tokens for IndieAuth flow
- SameSite cookies
- POST for state-changing operations
XSS Prevention
- Jinja2 auto-escaping enabled
- Markdown rendered to HTML server-side
- No user-generated HTML accepted
- CSP headers (future enhancement)
Development Mode Security
- DEV_MODE must be explicitly enabled
- Returns 404 for dev routes when disabled
- Prominent warnings when enabled
- Configuration validation prevents accidents
- Cannot coexist with production URL
Testing Strategy
Unit Tests
Public Routes (tests/test_routes_public.py):
- Test homepage renders
- Test note permalink
- Test 404 for unpublished notes
- Test microformats markup
Admin Routes (tests/test_routes_admin.py):
- Test login form renders
- Test dashboard requires auth
- Test create note flow
- Test edit note flow
- Test delete note flow
- Test logout
Dev Auth (tests/test_dev_auth.py):
- Test dev login creates session
- Test dev routes return 404 when disabled
- Test configuration validation
- Test dev mode indicators
Integration Tests
- Full authentication flow (mocked IndieLogin)
- Create note end-to-end
- Edit note end-to-end
- Delete note end-to-end
- Dev auth flow
Manual Tests
- Browser testing (Chrome, Firefox, Safari)
- Mobile responsive testing
- Microformats validation (indiewebify.me)
- Accessibility testing
- Real IndieLogin authentication
Performance Considerations
Response Times
- Homepage: < 200ms (database query + template render)
- Note page: < 200ms (database + file read + markdown render)
- Admin dashboard: < 200ms (database query + template)
Optimizations
- Database indexes on notes.created_at
- Single query per page (minimize DB roundtrips)
- Optional: template caching (Flask-Caching)
- Static assets cached by browser
Resource Usage
- Minimal memory (few templates in memory)
- No heavy JavaScript processing
- Server-side rendering reduces client load
Migration from Previous Phases
Database
- No schema changes needed
- All tables from Phase 3 remain
Code Changes
- Add route modules (new files)
- Add templates (new files)
- Add CSS (new file)
- Add dev_auth module (new file)
- Update config.py
- Update app.py to register routes
Configuration
- Add DEV_MODE and DEV_ADMIN_ME variables
- No breaking changes to existing config
Acceptance Criteria
Phase 4 is complete when:
Functional Requirements
- Public homepage displays recent notes
- Note permalinks work correctly
- Admin login works (IndieLogin)
- Admin dashboard displays all notes
- Create note form works
- Edit note form works
- Delete note works with confirmation
- Logout works correctly
- Dev auth works for local testing
- Flash messages display correctly
Security Requirements
- All admin routes require authentication
- Session cookies are secure
- Dev auth returns 404 when disabled
- Configuration validated on startup
- CSRF protection working
- No XSS vulnerabilities
Quality Requirements
- Test coverage > 90%
- All tests pass
- No linting errors (flake8)
- Code formatted (black)
- Templates validated (HTML5)
- Microformats validated
Documentation Requirements
- All routes documented
- Template structure documented
- Dev auth usage documented
- Security considerations documented
- Configuration documented
Performance Requirements
- Homepage loads < 200ms
- Note pages load < 200ms
- Admin pages load < 200ms
- Forms submit < 100ms
Implementation Checklist
Phase 4.1: Core Routes and Templates
- Create route modules (public, admin, auth)
- Implement public routes (/, /note/)
- Implement auth routes (login, callback, logout)
- Implement admin routes (dashboard, new, edit, delete)
- Create base templates (base.html, admin/base.html)
- Create public templates (index.html, note.html)
- Create admin templates (login, dashboard, new, edit)
- Test route functionality
Phase 4.2: Development Authentication
- Create dev_auth.py module
- Implement dev login route
- Add configuration validation
- Add dev mode warnings
- Add visual indicators
- Test dev auth functionality
- Document dev auth usage
Phase 4.3: Frontend Styling
- Create style.css with variables
- Style public interface
- Style admin interface
- Add responsive breakpoints
- Style flash messages
- Style forms and buttons
- Test on mobile devices
Phase 4.4: JavaScript Enhancements (Optional)
- Add marked.js preview (optional)
- Implement preview.js
- Test progressive enhancement
- Ensure graceful degradation
Phase 4.5: Testing and Validation
- Write route unit tests
- Write integration tests
- Write dev auth tests
- Validate microformats
- Validate HTML5
- Test accessibility
- Test real IndieLogin auth
- Security audit
Phase 4.6: Documentation
- Document routes
- Document templates
- Document CSS architecture
- Document dev auth
- Update CHANGELOG
- Increment version to 0.5.0
Success Metrics
Phase 4 is successful if:
- User can view published notes on public interface
- User can authenticate via IndieLogin
- Admin can create notes via web interface
- Admin can edit notes via web interface
- Admin can delete notes via web interface
- Developer can test locally without deployment
- All routes are secure and properly protected
- Templates render correctly with microformats
- Tests pass with >90% coverage
- Dev auth is safe and clearly separated
Dependencies
Python Packages (Existing)
- Flask (web framework)
- Jinja2 (templating, included with Flask)
- markdown (markdown rendering, existing)
- httpx (HTTP client, existing)
No New Dependencies Required
External Services
- IndieLogin.com (production auth)
- None for dev mode
Risk Assessment
Technical Risks
Risk: Dev auth accidentally enabled in production
- Likelihood: Medium
- Impact: Critical (authentication bypass)
- Mitigation: Configuration validation, startup warnings, visual indicators, documentation
Risk: Template XSS vulnerabilities
- Likelihood: Low
- Impact: High
- Mitigation: Jinja2 auto-escaping, no user HTML, code review
Risk: Session cookie theft
- Likelihood: Low
- Impact: High
- Mitigation: HttpOnly, Secure, SameSite cookies, HTTPS required
Operational Risks
Risk: User forgets to disable DEV_MODE
- Likelihood: Medium
- Impact: Critical
- Mitigation: Deployment checklist, visual warnings, config validation
Risk: Missing IndieLogin configuration
- Likelihood: Low
- Impact: Medium
- Mitigation: Startup validation, clear error messages, documentation
Future Enhancements (V2+)
Deferred features:
- Advanced note editor (WYSIWYG)
- Image uploads
- Draft saving (auto-save)
- Note preview before publish
- Bulk operations
- Search functionality
- Tags UI
- Custom themes
- Admin user management
- Activity log
- Session management UI
References
Internal Documentation
- ADR-003: Frontend Technology
- ADR-005: IndieLogin Authentication
- ADR-010: Authentication Module Design
- ADR-011: Development Authentication Mechanism
- Architecture Overview
- Phase 3 Report
External Standards
- Microformats2 h-entry
- HTML5 Specification
- Flask Documentation
- Jinja2 Documentation
- OWASP Web Security
Phase: 4 Version Target: 0.5.0 Status: Design Complete, Ready for Implementation Next Phase: Phase 5 (RSS Feed Generation)