v0.4.0: live feed updates (auto-insert new posts)

New artifacts appear at the top of the feed without a manual refresh.

- GET /feed/updates (session-authed): new artifacts since a timestamp marker,
  owner-scoped and filter-aware (source/label/kind/q), returned as row HTML.
- Feed JS polls every 5s, paused while the tab is hidden (immediate poll on
  refocus). Prepends new rows into today's group without touching the selected
  row or the preview iframe — what you're reading stays put and selected.
- Fresh rows show "now", aging into their real time after ~3 min; brief
  highlight on arrival.
- Extracted _artifact_rows.html partial, shared by the day-group render and the
  updates endpoint, so live rows match exactly.
- Polling chosen over SSE/WebSockets: zero infra on sync-WSGI + SQLite (see
  planning/next_steps for the SSE upgrade path). 142 tests pass; feed page +
  endpoint verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-04 23:02:07 +01:00
parent f6f004c689
commit cae414c307
9 changed files with 289 additions and 40 deletions

View file

@ -0,0 +1,34 @@
{% for artifact in artifacts %}
<a class="artifact-row {% if selected_artifact.id == artifact.id %}selected{% endif %} {% if artifact.is_unread %}unread{% endif %} {% if artifact.is_kept %}kept{% endif %} {% if artifact.is_shared %}shared{% endif %}"
href="?{% if selected_source %}source={{ selected_source|urlencode }}&amp;{% endif %}{% if selected_label %}label={{ selected_label|urlencode }}&amp;{% endif %}{% if selected_kind %}kind={{ selected_kind|urlencode }}&amp;{% endif %}{% if title_query %}q={{ title_query|urlencode }}&amp;{% endif %}artifact={{ artifact.id }}&amp;view={{ selected_view }}"
data-artifact-id="{{ artifact.id }}"
data-title="{{ artifact.title }}"
data-kind="{{ artifact.kind }}"
data-source="{{ artifact.source }}"
data-source-type="{{ artifact.source_type }}"
data-source-name="{{ artifact.source_name }}"
data-created="{{ artifact.created_at|date:'M j Y, H:i' }}"
data-size="{{ artifact.size_bytes|filesizeformat }}"
data-kept="{{ artifact.is_kept|yesno:'1,0' }}"
data-shared="{{ artifact.is_shared|yesno:'1,0' }}"
data-share-url="{% if artifact.is_shared %}{{ request.scheme }}://{{ request.get_host }}{% url 'artifact-shared' artifact.share_token %}{% endif %}"
data-tags="{{ artifact.tags|join:', ' }}"
data-rendered-url="{% url 'artifact' artifact.id %}"
data-raw-url="{% url 'artifact' artifact.id %}?raw=1">
<time class="artifact-time" datetime="{{ artifact.created_at|date:'c' }}">{{ artifact.created_at|date:"H:i" }}</time>
<span class="artifact-row-main">
<span class="artifact-title">{{ artifact.title }}</span>
<span class="artifact-kind-text">{% if artifact.kind == "image" %}image{% elif artifact.kind == "pdf" %}pdf{% elif artifact.kind == "markdown" %}markdown{% elif artifact.kind == "deck" %}deck{% elif artifact.kind == "binary" %}binary{% else %}html{% endif %}</span>
<span class="kept-badge" data-kept-badge title="Kept — never auto-deleted"{% if not artifact.is_kept %} hidden{% endif %}>K</span>
<span class="shared-badge" data-shared-badge title="Shared — anyone with the link can view"{% if not artifact.is_shared %} hidden{% endif %}>🔗</span>
{% if artifact.label_chips %}
<span class="tag-chips">
{% for label in artifact.label_chips %}
<span class="tag-chip" style="background:{{ label.color }}">{{ label.name }}</span>
{% endfor %}
</span>
{% endif %}
</span>
<span class="artifact-source">{{ artifact.source }}</span>
</a>
{% endfor %}