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>
115 lines
4.0 KiB
HTML
115 lines
4.0 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{% if query %}Search: {{ query }}{% else %}Search{% endif %} - StarPunk{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="search-container">
|
|
<!-- Search Header -->
|
|
<div class="search-header">
|
|
<h2>Search Results</h2>
|
|
{% if query %}
|
|
<p class="note-meta">
|
|
Found {{ results|length }} result{{ 's' if results|length != 1 else '' }}
|
|
for "<strong>{{ query }}</strong>"
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Search Form -->
|
|
<div class="search-form-container" style="background: var(--color-bg-alt); padding: var(--spacing-md); border-radius: var(--border-radius); margin-bottom: var(--spacing-lg);">
|
|
<form action="/search" method="get" role="search">
|
|
<div class="form-group" style="margin-bottom: 0;">
|
|
<div style="display: flex; gap: var(--spacing-sm);">
|
|
<input
|
|
type="search"
|
|
name="q"
|
|
placeholder="Enter search terms..."
|
|
value="{{ query }}"
|
|
minlength="2"
|
|
maxlength="100"
|
|
required
|
|
autofocus
|
|
style="flex: 1;"
|
|
>
|
|
<button type="submit" class="button button-primary">Search</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Results -->
|
|
{% if query %}
|
|
{% if error %}
|
|
<!-- Error state (if search unavailable) -->
|
|
<div class="flash flash-warning" role="alert">
|
|
<h3 style="margin-bottom: var(--spacing-sm);">Search Unavailable</h3>
|
|
<p>{{ error }}</p>
|
|
<p style="margin-bottom: 0; margin-top: var(--spacing-sm);">Full-text search is temporarily unavailable. Please try again later.</p>
|
|
</div>
|
|
{% elif results %}
|
|
<div class="search-results">
|
|
{% for result in results %}
|
|
<article class="search-result" style="margin-bottom: var(--spacing-lg); padding-bottom: var(--spacing-lg); border-bottom: 1px solid var(--color-border);">
|
|
<h3 style="margin-bottom: var(--spacing-sm);">
|
|
<a href="{{ result.url }}">{{ result.title }}</a>
|
|
</h3>
|
|
<div class="search-excerpt" style="margin-bottom: var(--spacing-sm);">
|
|
<!-- Excerpt with highlighted terms (safe because we control the <mark> tags) -->
|
|
<p style="margin-bottom: 0;">{{ result.excerpt|safe }}</p>
|
|
</div>
|
|
<div class="note-meta">
|
|
<time datetime="{{ result.published_at }}">
|
|
{{ result.published_at[:10] }}
|
|
</time>
|
|
</div>
|
|
</article>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Pagination (if more than limit results possible) -->
|
|
{% if results|length == limit %}
|
|
<nav aria-label="Search pagination" style="margin-top: var(--spacing-lg);">
|
|
<div style="display: flex; gap: var(--spacing-md); justify-content: center;">
|
|
{% if offset > 0 %}
|
|
<a class="button button-secondary" href="/search?q={{ query|urlencode }}&offset={{ [0, offset - limit]|max }}">
|
|
Previous
|
|
</a>
|
|
{% endif %}
|
|
<a class="button button-secondary" href="/search?q={{ query|urlencode }}&offset={{ offset + limit }}">
|
|
Next
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
{% endif %}
|
|
{% else %}
|
|
<!-- No results -->
|
|
<div class="flash flash-info" role="alert">
|
|
<h3 style="margin-bottom: var(--spacing-sm);">No results found</h3>
|
|
<p>Your search for "<strong>{{ query }}</strong>" didn't match any notes.</p>
|
|
<p style="margin-bottom: 0; margin-top: var(--spacing-sm);">Try different keywords or check your spelling.</p>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<!-- No query yet -->
|
|
<div class="empty-state">
|
|
<p style="font-size: 3rem; margin-bottom: var(--spacing-md);">🔍</p>
|
|
<p>Enter search terms above to find notes</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<style>
|
|
/* Search-specific styles */
|
|
mark {
|
|
background-color: #ffeb3b;
|
|
padding: 0.125rem 0.25rem;
|
|
border-radius: 2px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.search-result:last-child {
|
|
border-bottom: none;
|
|
}
|
|
</style>
|
|
{% endblock %}
|