The sidebar's per-feed nav depends on this query correctly scoping to
one feed (or joining all of them when feed_id is None), and it's real
branching logic rather than a passthrough — exactly what the updated
Definition of Done's testing rule calls for.
Uses a throwaway SQLite file per test (migrated fresh, cleaned up via
Drop) rather than a shared fixture, since diesel-async's bb8 pool would
otherwise hand out per-connection ":memory:" databases that don't share
state.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqTJmazHBQK878vjQ4JnnF
Default view stays the all-feeds joined article list, now with a sidebar
listing every subscribed feed so a reader can pin down to one feed's
articles. Filtering happens server-side (list_ranked_articles now takes
an optional feed_id).
Pulled in the sidebar/badge/scroll_area (plus their sheet/skeleton/
tooltip/separator dependencies) components via `dx components add`
instead of hand-rolling nav/tag/scroll markup, matching this project's
existing pattern of using the Dioxus component library over raw
elements (see button/input).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqTJmazHBQK878vjQ4JnnF
feedsignal-db's Db impl had grown into one 250-line block mixing feed
subscriptions, article storage/scoring, reading-event logging, and
topic-affinity persistence in a single file. Split each domain's
methods into its own module (mirroring the earlier web crate SRP
split), keeping lib.rs to just the Db struct and connection setup.
Pure move — no behavior or public API changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QmwX8eafbMstJvt8XPSqft
Feeds were already persisted, but nothing ever re-fetched them after
the initial subscribe — scoring.rs had a TODO where feed polling was
supposed to go. Add a feed_polling job that re-fetches every
subscribed feed, relies on articles.url's unique constraint to skip
ones already seen, and stamps last_fetched_at. Runs once on startup
(so reopening the app catches up immediately) and every 15 minutes
after, ahead of the scoring pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QmwX8eafbMstJvt8XPSqft
Separates controllers (api/, the #[server] endpoints) from business
services (server/services/), background scheduling (server/jobs/,
renamed from pipeline.rs), infra bootstrap/config (server/mod.rs,
server/config.rs), and view components (app/), each one file per
responsibility instead of the previous server.rs/app.rs/pipeline.rs
grab-bags.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoXS1ERDGC1P189RqmUxAF
- cargo fmt: reformat a match arm in app.rs and drop trailing blank
lines the dx-components generator left in two mod.rs files
- clippy -D warnings (feedsignal-web, server feature): the generated
ButtonVariant/ButtonSize enums have variants this app doesn't use yet
(they're part of the component library's API surface, not dead code
in the ordinary sense) — clippy's dead_code lint was failing the
build under -D warnings, fixed with a scoped #[allow(dead_code)]
Verified all three CI jobs locally: cargo fmt --check, clippy for
native crates + both feedsignal-web feature sets, cargo test
--workspace --exclude feedsignal-web, and cargo audit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJJQb2DWZZwQ1yPiaAQQoY
Separate tests for empty/whitespace/non-blank input so a regression
points at the exact case that broke instead of a single bundled test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJJQb2DWZZwQ1yPiaAQQoY
- Annotate the form onsubmit event type for clarity
- Disable the Subscribe button while the URL field is blank
- Rename title -> feed_title and invert the blank-title filter into a
positively-named has_content helper, with a unit test
- Guard fetch_feed with error_for_status() so a well-formed URL that
points at nothing (404/5xx) surfaces a clear error instead of an
opaque feed-rs parse failure
- Collapse subscribe_feed_impl's two upsert_feed writes into one: fetch
under a throwaway id first, then let upsert_feed be the single source
of truth for the real feed id (existing id on re-subscribe, fresh
otherwise), remapping fetched articles onto it before inserting. This
also means a failed subscribe no longer leaves a placeholder row behind.
- Move the blank-URL check ahead of trimming
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJJQb2DWZZwQ1yPiaAQQoY
Adds a URL input + Subscribe button (dx-components Input/Button) to the
UI, wired to a new subscribe_feed server function that upserts the feed
row and does an immediate first fetch so the reader isn't empty until
the next scheduled poll. feedsignal_feeds::fetch_feed now also returns
the feed's title, used to replace the URL placeholder once fetched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJJQb2DWZZwQ1yPiaAQQoY
cargo clippy --all-targets already type-checks everything cargo check
would, so running both for each of the three feature/target
combinations was roughly doubling compile time in this job.
Switches from the docker/rust:1-bookworm container back to the rust-ci
runner label, and adopts sporah's build/test/audit job split with
cargo registry + sccache caching. Keeps feedsignal's workspace-specific
checks (native crates, web crate server/wasm-client features) and adds
clippy + cargo audit, which were previously missing.
apply_feedback, decay, get_mean_affinity, and engagement_score had no
(or broken) test coverage. Also fixes decay(): subtracting a fixed
DAILY_DECAY from a score smaller than that step flipped its sign
instead of settling at zero, causing oscillation on repeated decay
passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcD2BtNqehJxchKiuodTxi
sqlx's query!/query_as! macros only check raw SQL strings against the live
schema; Diesel's table!-derived DSL type-checks query structure itself at
compile time. SQLite has no native async driver, so diesel-async wraps a
blocking SqliteConnection via SyncConnectionWrapper, pooled with bb8.
Migrations move from sqlx's single-file-per-migration format to Diesel's
up.sql/down.sql pairs, run transactionally via diesel_migrations against a
throwaway sync connection at boot (MigrationHarness needs a sync
Connection), giving revertable migrations that sqlx::migrate! doesn't
support.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was hardcoded to llama3.1, which isn't actually on this machine.
Switched to gemma4-e4b (5.0 GB, already pulled) as a speed/quality
balance for a call made once per shortlisted article on every
pipeline run.
Rust workspace with core (topic-affinity learning engine + relevance
scoring), db (sqlite/sqlx schema + repo), feeds (RSS/Atom fetch), llm
(rig + local Ollama embeddings/completion), and web (axum + Dioxus
fullstack UI, no separate JS stack). Two-stage relevance filtering
(embedding shortlist -> LLM judgment) and an engagement/surprise-based
topic affinity engine with daily decay. All crates compile and core's
affinity engine has passing unit tests; server and wasm client targets
of feedsignal-web both check clean.