9987da2c07
Add ``m.event_id`` (T109's nullable column from migration 0014) to
``search_all_memories``'s SELECT, propagate it through the route's
template context, and have ``search.html`` build result links as
``/chats/{chat_id}#turn-{event_id}`` — matching the ``id="turn-{event_id}"``
anchor that Phase 3.5 T86 stamps on each turn DOM node so the chat page
scrolls to the originating turn on load. Memory rows projected before
the 0014 migration ran read NULL ``event_id``; the template falls back
to a chat-level link in that case so we never emit ``#turn-None``.
Pre-existing tests that asserted on the bare ``href="/chats/{chat_id}"``
contract are updated to assert on the ``href="/chats/{chat_id}#turn-``
prefix to reflect the new deep-link.
53 lines
2.3 KiB
HTML
53 lines
2.3 KiB
HTML
{% extends "layout.html" %}
|
|
{% block title %}Search - chat{% endblock %}
|
|
{% block content %}
|
|
<header class="page-header">
|
|
<h1>Search</h1>
|
|
</header>
|
|
|
|
<form class="search-page-form" action="/search" method="get">
|
|
<input type="text" name="q" value="{{ query|default('', true) }}"
|
|
placeholder="Search memories across all chats" autofocus>
|
|
<button type="submit">Search</button>
|
|
</form>
|
|
|
|
{% if not query %}
|
|
{# Empty-state placeholder: the top-bar form submits to /search even
|
|
with no input, so this page must render cleanly with no query. #}
|
|
<p class="muted search-empty">Enter a query to search memories across all chats.</p>
|
|
{% elif not results %}
|
|
<p class="muted">No matches for “{{ query }}”.</p>
|
|
{% else %}
|
|
<ul class="search-results">
|
|
{% for r in results %}
|
|
<li class="search-result">
|
|
{# T111.2: deep-link to the originating turn via the
|
|
``id="turn-{event_id}"`` anchor stamped by Phase 3.5 T86.
|
|
``event_id`` may be NULL for memory rows projected before the
|
|
0014 migration ran (T109 did not backfill historical rows); in
|
|
that case fall back to a chat-level link with no anchor so we
|
|
never emit ``#turn-None``. #}
|
|
<a class="search-result-link"
|
|
href="/chats/{{ r.chat_id }}{% if r.event_id %}#turn-{{ r.event_id }}{% endif %}">
|
|
<div class="search-result-meta muted">
|
|
<strong>{{ r.owner_name }}</strong>
|
|
<span>· {{ r.chat_id }}</span>
|
|
{% if r.chat_name %}<span>· {{ r.chat_name }}</span>{% endif %}
|
|
{% if r.scene_label %}<span>· scene {{ r.scene_label }}</span>{% endif %}
|
|
</div>
|
|
{# T111.1: ``r.snippet`` is the FTS5 ``snippet()`` excerpt with
|
|
each match wrapped in ``<mark>...</mark>``. ``|safe`` is
|
|
required so the marker tags survive Jinja's auto-escape; the
|
|
snippet is built by SQLite from indexed text, so the only
|
|
HTML in the string is the ``<mark>`` we configured (any
|
|
special chars from the source content are passed through as
|
|
literal text, NOT as HTML). This is the only ``|safe`` filter
|
|
on the page — chat_id, owner_name, etc. remain auto-escaped. #}
|
|
<div class="search-result-summary">{{ r.snippet|safe }}</div>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
{% endblock %}
|