v0.6.0 — feeds-beta (Phase 4: RSS & imports)
[0.6.0] — feeds-beta (Phase 4: RSS & imports)
Recurring ingestion arrives: subscribe to RSS/Atom feeds, bulk-import an
existing reading library, and save straight from the browser with a selection
captured as a highlight. STATIC_VERSION 66 → 67.
Added — RSS & feeds
- Feed subscriptions (migration 013:
feeds+feed_entries). Subscribe
by feed URL or page URL —POST /api/feedsautodiscovers a
<link rel="alternate" type="application/rss+xml|atom+xml">when the URL is
an HTML page (30s timeout, 5-redirect cap, 10 MB body cap; 409
already_subscribedon a duplicate feed URL). Feeds carry a per-feed
fetch_interval_minutes, conditional-GET validators (etag/last-modified),
and anerror_countbackoff. - Recurring poll loop on the new
PeriodicTaskscheduler registry
(tiro/scheduler.py, extended not replaced — the imap/digest/vector loops
were refactored onto it). Each poll cycle writes one audit line. New articles
land withingestion_method="rss"; thefeed_entriesdedup ledger keeps a
deleted article from being resurrected by the next poll (itsarticle_idis
nulled, the ledger row survives). - Feed management (
GET/POST /api/feeds,PATCH/DELETE /api/feeds/{id},
POST /api/feeds/{id}/check,POST /api/feeds/check-all) and a/feeds
management page (grouped by folder, status pills, check-now / pause / rename /
delete-with-or-without-articles).DELETE ?delete_articles=truetakes an
auto_backupfirst, then loops thedelete_articlelifecycle coordinator
per article — never a raw cascade. Sidebar Library entry + Shift+F
keyboard shortcut (inbox & reader;nwas already taken). - OPML round-trip:
GET /api/feeds/export(standalone OPML 2.0, nested one
level by folder) andPOST /api/feeds/import(multipart upload, flattens
nested outlines into afolderpath, dedupes by url, returns
{added, skipped, errors}; rejects >5 MB or unparseable).
Added — library importers
- Three importers — Readwise JSON, Instapaper CSV, Omnivore zip — via
POST /api/import/{kind}(single-slot background job; 409import_running
when one is active;GET /api/import/statuspolls progress) and CLI verbs
tiro import-readwise|import-instapaper|import-omnivore(always skip
existing). Content is re-fetched where possible; a paywalled/failed re-fetch
falls back to a stub article taggedimport-stub. Original timestamps are
preserved. Imported articles useingestion_method="import". - Anchored highlight import: Readwise highlights are anchored against the
re-fetched markdown body with the same D7.4 machinery the reader uses;
unlocatable highlights are skipped and counted, never hand-placed. A
Settings "Import library" card drives it with a live progress bar.
Added — Chrome extension advanced save
- Background service worker registers three context-menu items (Save /
Save as VIP / Save with selection as highlight) and the popup gains a
save-all-open-tabs action. Selecting text and saving anchors it as a
highlight server-side viahighlight_textonPOST /api/ingest/url
(soft-fails to no-highlight, still 200, if the selection can't be located).
Added — owner UX wave
- Reading progress bar in the reader (fixed, accent fill, both themes; a
ResizeObserverre-measures when late-loading images reflow the body). - Unread-first inbox with a Library view toggle (
akey /?view=library)
that reveals read + archived rows — read/unread and active/decayed are
treated as orthogonal axes.
Added — export & backup
metadata.jsongains an additivefeedskey (durable subscription columns
only; transient fetch state and thefeed_entriesledger excluded).
sources.opmlmarks feed-backed sources withtype="rss"+xmlUrl.
tiro importmerges bundle feeds by url;tiro backup/restoreround-trips
feed rows wholesale. See EXPORT_SCHEMA.md.
Fixed
load_config()now honorsTIRO_CONFIG(ON-8 root-cause hardening).
A bareload_config()(fromtiro/app.py,scripts/, or any script run
from the repo root) previously ignored theTIRO_CONFIGenv var and
defaulted to CWD-relative./config.yaml, so a load→persist round-trip
could silently corrupt the owner's real config. Path precedence is now
explicit-arg >TIRO_CONFIG>./config.yaml, matchingrun.py/cli.py/
the MCP server. No signature change (additive default →None).
Added — iOS device pairing
- Device pairing for the iOS client (
/setup/qr?mode=device,
POST /api/auth/pair). The/setup/qrpage becomes two labeled panels —
browser sign-in (unchangedlogin/qrQR) and app pairing — the latter
encoding atiro://pair?url=…&code=…QR the native app scans in-app to
exchange a one-time code for a long-livedios:<device_name>API token.
Mirrors the QR-login token machinery exactly (sha256-only storage,
15-minute TTL, atomic single-use consume, generic-400 failures, no-store);
newdevice_pair_codestable (migration 012); doctor purges expired/used
codes in the same housekeeping bucket as login tokens.
Design pass — full frontend redesign (design/codex-pass, STATIC_VERSION 66)
Whole-app visual and interaction redesign, landed as a sequence of
self-contained tasks (icons/tokens/chrome first, then a page-by-page pass,
then motion/dark-theme/glyph closeout). No backend or API surface changed.
- Icons: a single canonical SVG icon set (
tiro/frontend/static/js/icons.js
for JS call sites,_icons.html'sicon()Jinja macro for templates, kept
in sync by a dedicated test) replaces every emoji/dingbat/HTML-entity glyph
across the app — nav, toolbars, modals, cards, empty states, close buttons. - Chrome: CSS custom-property token layer (color/spacing/radius/type
scale, light + dark) and shared component primitives (modal, overlay,
banner surfaces); rebuilt 240px sidebar with an icon rail; phone chrome
replaced with a bottom tab bar + Library/More sheets (hamburger menu
removed). - Per-page pass: inbox (icon cards, toolbar, serif pagination, empty
states), reader (chrome, audio player, callouts, phone action bar), digest,
stats, graph, sources, wiki, highlights, settings, login, QR/remote-access
wizards, and the offline fallback page all restyled on the new token/
component layer. - Motion: a consistent transition/easing pass across interactive surfaces
(cards, modals, sheets, toasts) withprefers-reduced-motion: reduce
support throughout, plus a dark-theme (Roman Night) contrast audit. - Closeout sweep: final glyph audit — remaining literal
×close
buttons (graph.html's node panel, base.html's LAN-over-HTTP banner
dismiss, reader.html's analysis/highlights panel close buttons) converted
to the canonicalcloseicon; orphaned.shortcuts-closeand
.graph-node-panel-closeCSS rules removed (both close buttons already
used.modal-close); LAN-banner phone padding constant aligned to the
real phone-header height.STATIC_VERSION65 → 66.