Files
StarPunk/starpunk/routes/public.py
Phil Skentelbery 07fff01fab feat: Complete v1.1.1 Phases 2 & 3 - Enhancements and Polish
Phase 2 - Enhancements:
- Add performance monitoring infrastructure with MetricsBuffer
- Implement three-tier health checks (/health, /health?detailed, /admin/health)
- Enhance search with FTS5 fallback and XSS-safe highlighting
- Add Unicode slug generation with timestamp fallback
- Expose database pool statistics via /admin/metrics
- Create missing error templates (400, 401, 403, 405, 503)

Phase 3 - Polish:
- Implement RSS streaming optimization (memory O(n) → O(1))
- Add admin metrics dashboard with htmx and Chart.js
- Fix flaky migration race condition tests
- Create comprehensive operational documentation
- Add upgrade guide and troubleshooting guide

Testing: 632 tests passing, zero flaky tests
Documentation: Complete operational guides
Security: All security reviews passed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 20:10:41 -07:00

149 lines
4.7 KiB
Python

"""
Public routes for StarPunk
Handles public-facing pages including homepage and note permalinks.
No authentication required for these routes.
"""
import hashlib
from datetime import datetime, timedelta
from flask import Blueprint, abort, render_template, Response, current_app
from starpunk.notes import list_notes, get_note
from starpunk.feed import generate_feed_streaming
# Create blueprint
bp = Blueprint("public", __name__)
# Simple in-memory cache for RSS feed note list
# Caches the database query results to avoid repeated DB hits
# XML is streamed, not cached (memory optimization for large feeds)
# Structure: {'notes': list[Note], 'timestamp': datetime}
_feed_cache = {"notes": None, "timestamp": None}
@bp.route("/")
def index():
"""
Homepage displaying recent published notes
Returns:
Rendered homepage template with note list
Template: templates/index.html
Microformats: h-feed containing h-entry items
"""
# Get recent published notes (limit 20)
notes = list_notes(published_only=True, limit=20)
return render_template("index.html", notes=notes)
@bp.route("/note/<slug>")
def note(slug: str):
"""
Individual note permalink page
Args:
slug: URL-safe note identifier
Returns:
Rendered note template with full content
Raises:
404: If note not found or not published
Template: templates/note.html
Microformats: h-entry
"""
# Get note by slug
note_obj = get_note(slug=slug)
# Return 404 if note doesn't exist or isn't published
if not note_obj or not note_obj.published:
abort(404)
return render_template("note.html", note=note_obj)
@bp.route("/feed.xml")
def feed():
"""
RSS 2.0 feed of published notes
Generates standards-compliant RSS 2.0 feed using memory-efficient streaming.
Instead of building the entire feed in memory, yields XML chunks directly
to the client for optimal memory usage with large feeds.
Cache duration is configurable via FEED_CACHE_SECONDS (default: 300 seconds
= 5 minutes). Cache stores note list to avoid repeated database queries,
but streaming prevents holding full XML in memory.
Returns:
Streaming XML response with RSS feed
Headers:
Content-Type: application/rss+xml; charset=utf-8
Cache-Control: public, max-age={FEED_CACHE_SECONDS}
Streaming Strategy:
- Database query cached (avoid repeated DB hits)
- XML generation streamed (avoid full XML in memory)
- Client-side: Cache-Control header with max-age
Performance:
- Memory usage: O(1) instead of O(n) for feed size
- Latency: Lower time-to-first-byte (TTFB)
- Recommended for feeds with 100+ items
Examples:
>>> # Request streams XML directly to client
>>> response = client.get('/feed.xml')
>>> response.status_code
200
>>> response.headers['Content-Type']
'application/rss+xml; charset=utf-8'
"""
# Get cache duration from config (in seconds)
cache_seconds = current_app.config.get("FEED_CACHE_SECONDS", 300)
cache_duration = timedelta(seconds=cache_seconds)
now = datetime.utcnow()
# Check if note list cache is valid
# We cache the note list to avoid repeated DB queries, but still stream the XML
if _feed_cache["notes"] and _feed_cache["timestamp"]:
cache_age = now - _feed_cache["timestamp"]
if cache_age < cache_duration:
# Use cached note list
notes = _feed_cache["notes"]
else:
# Cache expired, fetch fresh notes
max_items = current_app.config.get("FEED_MAX_ITEMS", 50)
notes = list_notes(published_only=True, limit=max_items)
_feed_cache["notes"] = notes
_feed_cache["timestamp"] = now
else:
# No cache, fetch notes
max_items = current_app.config.get("FEED_MAX_ITEMS", 50)
notes = list_notes(published_only=True, limit=max_items)
_feed_cache["notes"] = notes
_feed_cache["timestamp"] = now
# Generate streaming response
# This avoids holding the full XML in memory - chunks are yielded directly
max_items = current_app.config.get("FEED_MAX_ITEMS", 50)
generator = generate_feed_streaming(
site_url=current_app.config["SITE_URL"],
site_name=current_app.config["SITE_NAME"],
site_description=current_app.config.get("SITE_DESCRIPTION", ""),
notes=notes,
limit=max_items,
)
# Return streaming response with appropriate headers
response = Response(generator, mimetype="application/rss+xml; charset=utf-8")
response.headers["Cache-Control"] = f"public, max-age={cache_seconds}"
return response