Implements full search functionality for StarPunk v1.1.0. Search API Endpoint (/api/search): - GET endpoint with query parameter (q) validation - Pagination via limit (default 20, max 100) and offset parameters - JSON response with results count and formatted search results - Authentication-aware: anonymous users see published notes only - Graceful handling of FTS5 unavailability (503 error) - Proper error responses for missing/empty queries Search Web Interface (/search): - HTML search results page with Bootstrap-inspired styling - Search form with HTML5 validation (minlength=2, maxlength=100) - Results display with title, excerpt, date, and links - Empty state for no results - Error state for FTS5 unavailability - Simple pagination (Next/Previous navigation) Navigation Integration: - Added search box to site navigation in base.html - Preserves query parameter on results page - Responsive design with emoji search icon - Accessible with proper ARIA labels FTS Index Population: - Added startup check in __init__.py for empty FTS index - Automatic rebuild from existing notes on first run - Graceful degradation if population fails - Logging for troubleshooting Security Features: - XSS prevention: HTML in search results properly escaped - Safe highlighting: FTS5 <mark> tags preserved, user content escaped - Query validation: empty queries rejected, length limits enforced - SQL injection prevention via FTS5 query parser - Authentication filtering: unpublished notes hidden from anonymous users Testing: - Added 41 comprehensive tests across 3 test files - test_search_api.py: 12 tests for API endpoint validation - test_search_integration.py: 17 tests for UI rendering and integration - test_search_security.py: 12 tests for XSS, SQL injection, auth filtering - All tests passing with no regressions Implementation follows architect specifications from: - docs/architecture/v1.1.0-validation-report.md - docs/architecture/v1.1.0-feature-architecture.md - docs/decisions/ADR-034-full-text-search.md Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
183 lines
5.3 KiB
Python
183 lines
5.3 KiB
Python
"""
|
|
StarPunk package initialization
|
|
Creates and configures the Flask application
|
|
"""
|
|
|
|
import logging
|
|
from flask import Flask
|
|
|
|
|
|
def configure_logging(app):
|
|
"""
|
|
Configure application logging based on LOG_LEVEL
|
|
|
|
Args:
|
|
app: Flask application instance
|
|
"""
|
|
log_level = app.config.get("LOG_LEVEL", "INFO").upper()
|
|
|
|
# Set Flask logger level
|
|
app.logger.setLevel(getattr(logging, log_level, logging.INFO))
|
|
|
|
# Configure handler with detailed format for DEBUG
|
|
handler = logging.StreamHandler()
|
|
|
|
if log_level == "DEBUG":
|
|
formatter = logging.Formatter(
|
|
"[%(asctime)s] %(levelname)s - %(name)s: %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
|
|
# Warn if DEBUG enabled in production
|
|
if not app.debug and app.config.get("ENV") != "development":
|
|
app.logger.warning(
|
|
"=" * 70
|
|
+ "\n"
|
|
+ "WARNING: DEBUG logging enabled in production!\n"
|
|
+ "This logs detailed HTTP requests/responses.\n"
|
|
+ "Sensitive data is redacted, but consider using INFO level.\n"
|
|
+ "Set LOG_LEVEL=INFO in production for normal operation.\n"
|
|
+ "=" * 70
|
|
)
|
|
else:
|
|
formatter = logging.Formatter(
|
|
"[%(asctime)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
)
|
|
|
|
handler.setFormatter(formatter)
|
|
|
|
# Remove existing handlers and add our configured handler
|
|
app.logger.handlers.clear()
|
|
app.logger.addHandler(handler)
|
|
|
|
|
|
def create_app(config=None):
|
|
"""
|
|
Application factory for StarPunk
|
|
|
|
Args:
|
|
config: Optional configuration dict to override defaults
|
|
|
|
Returns:
|
|
Configured Flask application instance
|
|
"""
|
|
app = Flask(__name__, static_folder="../static", template_folder="../templates")
|
|
|
|
# Load configuration
|
|
from starpunk.config import load_config
|
|
|
|
load_config(app, config)
|
|
|
|
# Configure logging
|
|
configure_logging(app)
|
|
|
|
# Initialize database
|
|
from starpunk.database import init_db
|
|
|
|
init_db(app)
|
|
|
|
# Initialize FTS index if needed
|
|
from pathlib import Path
|
|
from starpunk.search import has_fts_table, rebuild_fts_index
|
|
import sqlite3
|
|
|
|
db_path = Path(app.config["DATABASE_PATH"])
|
|
data_path = Path(app.config["DATA_PATH"])
|
|
|
|
if has_fts_table(db_path):
|
|
# Check if index is empty (fresh migration or first run)
|
|
try:
|
|
conn = sqlite3.connect(db_path)
|
|
count = conn.execute("SELECT COUNT(*) FROM notes_fts").fetchone()[0]
|
|
conn.close()
|
|
|
|
if count == 0:
|
|
app.logger.info("FTS index is empty, populating from existing notes...")
|
|
try:
|
|
rebuild_fts_index(db_path, data_path)
|
|
app.logger.info("FTS index successfully populated")
|
|
except Exception as e:
|
|
app.logger.error(f"Failed to populate FTS index: {e}")
|
|
except Exception as e:
|
|
app.logger.debug(f"FTS index check skipped: {e}")
|
|
|
|
# Register blueprints
|
|
from starpunk.routes import register_routes
|
|
|
|
register_routes(app)
|
|
|
|
# Error handlers
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
from flask import render_template, request
|
|
|
|
# Return HTML for browser requests, JSON for API requests
|
|
if request.path.startswith("/api/"):
|
|
return {"error": "Not found"}, 404
|
|
return render_template("404.html"), 404
|
|
|
|
@app.errorhandler(500)
|
|
def server_error(error):
|
|
from flask import render_template, request
|
|
|
|
# Return HTML for browser requests, JSON for API requests
|
|
if request.path.startswith("/api/"):
|
|
return {"error": "Internal server error"}, 500
|
|
return render_template("500.html"), 500
|
|
|
|
# Health check endpoint for containers and monitoring
|
|
@app.route("/health")
|
|
def health_check():
|
|
"""
|
|
Health check endpoint for containers and monitoring
|
|
|
|
Returns:
|
|
JSON with status and basic info
|
|
|
|
Response codes:
|
|
200: Application healthy
|
|
500: Application unhealthy
|
|
|
|
Checks:
|
|
- Database connectivity
|
|
- File system access
|
|
- Basic application state
|
|
"""
|
|
from flask import jsonify
|
|
import os
|
|
|
|
try:
|
|
# Check database connectivity
|
|
from starpunk.database import get_db
|
|
|
|
db = get_db(app)
|
|
db.execute("SELECT 1").fetchone()
|
|
db.close()
|
|
|
|
# Check filesystem access
|
|
data_path = app.config.get("DATA_PATH", "data")
|
|
if not os.path.exists(data_path):
|
|
raise Exception("Data path not accessible")
|
|
|
|
return (
|
|
jsonify(
|
|
{
|
|
"status": "healthy",
|
|
"version": app.config.get("VERSION", __version__),
|
|
"environment": app.config.get("ENV", "unknown"),
|
|
}
|
|
),
|
|
200,
|
|
)
|
|
|
|
except Exception as e:
|
|
return jsonify({"status": "unhealthy", "error": str(e)}), 500
|
|
|
|
return app
|
|
|
|
|
|
# Package version (Semantic Versioning 2.0.0)
|
|
# See docs/standards/versioning-strategy.md for details
|
|
__version__ = "1.1.0"
|
|
__version_info__ = (1, 1, 0)
|