Files
StarPunk/docs/design/phase-4-web-interface.md
Phil Skentelbery 0cca8169ce feat: Implement Phase 4 Web Interface with bugfixes (v0.5.2)
## 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>
2025-11-18 23:01:53 -07:00

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:

  1. User can view published notes on public interface
  2. User can authenticate via IndieLogin
  3. Admin can create notes via web interface
  4. Admin can edit notes via web interface
  5. Admin can delete notes via web interface
  6. Developer can test locally without deployment
  7. All routes are secure and properly protected
  8. Templates render correctly with microformats
  9. Tests pass with >90% coverage
  10. 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

External Standards


Phase: 4 Version Target: 0.5.0 Status: Design Complete, Ready for Implementation Next Phase: Phase 5 (RSS Feed Generation)