feat(search): Add complete Search UI with API and web interface
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>
This commit is contained in:
114
templates/search.html
Normal file
114
templates/search.html
Normal file
@@ -0,0 +1,114 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user