Skip to main content

max / balanced_breakfast

Theme standardization, plugin sandboxing, sync service split, integration tests Map all 14 TOML theme slots (GO-style COLOR_MAP), add contrastColor() for text-on-accent, theme import/export commands, fix hardcoded colors in HTML export. Rename CSS vars (--accent→--accent-red, --yolk→--accent-yellow, --success→--accent-green), add --accent-blue/--accent-purple/--accent-cyan/ --bg-surface/--text-on-accent. Primary actions use accent-blue, logo uses accent-blue. Also: Rhai plugin sandbox hardening, sync service modularization, split command_integration.rs into per-domain test files, feed generator split, bb-interface error types, navigation JS, OTA updater, mobile layout improvements, query feeds, new API endpoints, docs updates.
Co-Authored-By
Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-08 20:12 UTC
Commit: 319bc1e996641c851c1c90d046e6370ac1ccfdc5
Parent: 2a421da
77 files changed, +8223 insertions, -4740 deletions
M Cargo.lock +14 -12
@@ -210,6 +210,7 @@
210 210 "toml 0.8.2",
211 211 "tracing",
212 212 "tracing-subscriber",
213 + "ureq",
213 214 "uuid",
214 215 ]
215 216
@@ -296,6 +297,7 @@
296 297 dependencies = [
297 298 "chrono",
298 299 "serde",
300 + "serde_json",
299 301 ]
300 302
301 303 [[package]]
@@ -918,7 +920,7 @@
918 920 "libc",
919 921 "option-ext",
920 922 "redox_users",
921 - "windows-sys 0.61.2",
923 + "windows-sys 0.59.0",
922 924 ]
923 925
924 926 [[package]]
@@ -1086,7 +1088,7 @@
1086 1088 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
1087 1089 dependencies = [
1088 1090 "libc",
1089 - "windows-sys 0.61.2",
1091 + "windows-sys 0.59.0",
1090 1092 ]
1091 1093
1092 1094 [[package]]
@@ -2639,7 +2641,7 @@
2639 2641 source = "registry+https://github.com/rust-lang/crates.io-index"
2640 2642 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
2641 2643 dependencies = [
2642 - "windows-sys 0.61.2",
2644 + "windows-sys 0.59.0",
2643 2645 ]
2644 2646
2645 2647 [[package]]
@@ -3024,7 +3026,7 @@
3024 3026 checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
3025 3027 dependencies = [
3026 3028 "libc",
3027 - "windows-sys 0.61.2",
3029 + "windows-sys 0.45.0",
3028 3030 ]
3029 3031
3030 3032 [[package]]
@@ -3883,7 +3885,7 @@
3883 3885 "errno",
3884 3886 "libc",
3885 3887 "linux-raw-sys",
3886 - "windows-sys 0.61.2",
3888 + "windows-sys 0.59.0",
3887 3889 ]
3888 3890
3889 3891 [[package]]
@@ -3940,7 +3942,7 @@
3940 3942 "security-framework",
3941 3943 "security-framework-sys",
3942 3944 "webpki-root-certs",
3943 - "windows-sys 0.61.2",
3945 + "windows-sys 0.59.0",
3944 3946 ]
3945 3947
3946 3948 [[package]]
@@ -3951,9 +3953,9 @@
3951 3953
3952 3954 [[package]]
3953 3955 name = "rustls-webpki"
3954 - version = "0.103.9"
3956 + version = "0.103.10"
3955 3957 source = "registry+https://github.com/rust-lang/crates.io-index"
3956 - checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
3958 + checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
3957 3959 dependencies = [
3958 3960 "ring",
3959 3961 "rustls-pki-types",
@@ -4939,9 +4941,9 @@
4939 4941
4940 4942 [[package]]
4941 4943 name = "tar"
4942 - version = "0.4.44"
4944 + version = "0.4.45"
4943 4945 source = "registry+https://github.com/rust-lang/crates.io-index"
4944 - checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
4946 + checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
4945 4947 dependencies = [
4946 4948 "filetime",
4947 4949 "libc",
@@ -5305,7 +5307,7 @@
5305 5307 "getrandom 0.3.4",
5306 5308 "once_cell",
5307 5309 "rustix",
5308 - "windows-sys 0.61.2",
5310 + "windows-sys 0.59.0",
5309 5311 ]
5310 5312
5311 5313 [[package]]
@@ -6283,7 +6285,7 @@
6283 6285 source = "registry+https://github.com/rust-lang/crates.io-index"
6284 6286 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
6285 6287 dependencies = [
6286 - "windows-sys 0.61.2",
6288 + "windows-sys 0.48.0",
6287 6289 ]
6288 6290
6289 6291 [[package]]
M README.md +47 -12
@@ -24,7 +24,7 @@
24 24 # Development (hot-reload frontend, debug backend)
25 25 cargo tauri dev
26 26
27 - # Production build
27 + # Production build (macOS DMG, Windows installer, Linux AppImage)
28 28 cargo tauri build
29 29
30 30 # Run all workspace tests
@@ -45,6 +45,22 @@
45 45
46 46 Dependency flow: `bb-interface` is leaf (no internal deps) -> `bb-core` and `bb-feed` depend on `bb-interface` -> `bb-db` depends on `bb-interface` -> `src-tauri` depends on all four.
47 47
48 + Shared libraries from `../Shared/`: [theme-common](../Shared/theme-common/) (theme loading), [tagtree](../Shared/tagtree/) (tag validation), [synckit-client](../Shared/synckit-client/) (cloud sync SDK).
49 +
50 + ## Features
51 +
52 + - **Unified timeline** -- RSS, Atom, JSON Feed, Hacker News, arXiv, and custom sources merged into one feed
53 + - **Plugin system** -- Rhai scripting for extensible feed fetching (write a plugin for any source)
54 + - **Reader view** -- clean article rendering with HTML sanitization via DocEngine
55 + - **Search** -- FTS5 full-text search across all items with sanitized indexing
56 + - **Organization** -- tags, starred items, read/unread tracking, query feeds (saved dynamic filters)
57 + - **Auto-fetch scheduling** -- configurable per-plugin fetch intervals, circuit breaker after consecutive failures
58 + - **Feed health tracking** -- visual status indicators (green/yellow/red) per source
59 + - **Cloud sync** -- SyncKit integration with E2E encryption (feeds, tags, read state, preferences)
60 + - **OTA updates** -- background update checker with consent dialog (Tauri updater v2)
61 + - **17 bundled themes** -- dark, light, and high-contrast variants in TOML format, system auto-detection
62 + - **Platforms** -- macOS, Windows, Linux (native via Tauri 2)
63 +
48 64 ## Plugin Authoring
49 65
50 66 Plugins ("bussers") are `.rhai` script files. Drop one into the plugins directory and it loads on next launch.
@@ -54,19 +70,38 @@
54 70
55 71 Every plugin defines four functions (`id`, `name`, `config_schema`, `fetch`) plus an optional `capabilities()`. Full authoring guide with field types, return shapes, host functions, and examples: [docs/plugin_authoring.md](docs/plugin_authoring.md).
56 72
57 - ## Features
58 -
59 - - **Unified timeline** -- RSS, Hacker News, arXiv, and custom sources merged into one feed
60 - - **Plugin system** -- Rhai scripting for extensible feed fetching (write a plugin for any source)
61 - - **Reader view** -- clean article rendering with HTML sanitization
62 - - **Search** -- FTS5 full-text search across all items
63 - - **Organization** -- tags, starred items, read/unread tracking, query feeds (saved filters)
64 - - **Cloud sync** -- SyncKit integration with E2E encryption (feeds, tags, read state, preferences)
65 - - **Themes** -- light and dark themes, system auto-detection
66 -
67 73 ## Bundled Plugins
68 74
69 - Four plugins ship with the app: **rss.rhai** (RSS/Atom/JSON Feed), **hackernews.rhai** (HN stories), **arxiv.rhai** (arXiv papers), **reader.rhai** (web page reader view).
75 + Eleven plugins ship with the app:
76 +
77 + | Plugin | Source |
78 + |--------|--------|
79 + | `rss.rhai` | RSS, Atom, and JSON Feed |
80 + | `hackernews.rhai` | Hacker News stories |
81 + | `arxiv.rhai` | arXiv papers |
82 + | `reader.rhai` | Web page reader view |
83 + | `github_trending.rhai` | GitHub trending repositories |
84 + | `devto.rhai` | Dev.to articles |
85 + | `lobsters.rhai` | Lobsters community |
86 + | `xkcd.rhai` | XKCD comics |
87 + | `nasa_apod.rhai` | NASA Astronomy Picture of the Day |
88 + | `earthquakes.rhai` | USGS earthquake data |
89 + | `nws_alerts.rhai` | National Weather Service alerts |
90 +
91 + ## Key Paths
92 +
93 + | What | Where |
94 + |------|-------|
95 + | Plugin contract types | `crates/bb-interface/src/` |
96 + | Plugin runtime + orchestrator | `crates/bb-core/src/` |
97 + | Feed aggregation | `crates/bb-feed/src/` |
98 + | Database layer | `crates/bb-db/src/` |
99 + | Tauri commands | `src-tauri/src/commands/` |
100 + | Frontend JS | `src-tauri/frontend/js/` |
101 + | Styles | `src-tauri/frontend/css/styles.css` |
102 + | Bundled plugins | `plugins/` |
103 + | Plugin authoring guide | `docs/plugin_authoring.md` |
104 + | Architecture | `docs/architecture.md` |
70 105
71 106 ## License
72 107
@@ -105,7 +105,7 @@
105 105
106 106 ## Database Layer
107 107
108 - SQLite via sqlx with compile-time migrations (9 migrations). The `Database` struct holds a connection pool (`max_connections: 5`) and provides typed repository accessors.
108 + SQLite via sqlx with compile-time migrations (10 migrations). The `Database` struct holds a connection pool (`max_connections: 16`) and provides typed repository accessors.
109 109
110 110 ### Tables
111 111
@@ -207,7 +207,7 @@
207 207 | Ordering/filtering | `crates/bb-feed/src/ordering.rs` |
208 208 | Database layer | `crates/bb-db/src/` |
209 209 | Repositories | `crates/bb-db/src/repository.rs` |
210 - | Migrations | `migrations/sqlite/` (001-009) |
210 + | Migrations | `migrations/sqlite/` (001-010) |
211 211 | Tauri app state | `src-tauri/src/state.rs` |
212 212 | Tauri commands | `src-tauri/src/commands/` |
213 213 | Sync service | `src-tauri/src/sync_service.rs` |
@@ -76,142 +76,9 @@
76 76 | JS LOC | 1,658 | 1,658 | 1,658 | 1,658 | 2,140 | 2,140 | 2,140 | 2,140 | 2,140 |
77 77 | Cold spots | 5 | 5 | 2 | 8 | 5 | 4 | 4 | 1 | 0 |
78 78
79 - ## Changes Since Last Audit
79 + ---
80 80
81 - ### Eleventh audit (2026-03-28, Run 12 cross-project)
82 - - **Test count:** 602 (547 Rust + 55 JS). 0 clippy warnings. 0 failures.
83 - - **Grade:** A (maintained). v0.3.0.
84 - - **No code changes since Run 9.**
85 - - **New dependency advisories:**
86 - - rustls-webpki 0.103.9 (RUSTSEC-2026-0049) — upgrade to 0.103.10 via `cargo update -p rustls-webpki`
87 - - tar 0.4.44 x2 (RUSTSEC-2026-0067 symlink chmod, -0068 PAX size) — upgrade to 0.4.45 via `cargo update -p tar`. Via tauri-plugin-updater, relevant to OTA.
88 - - **Mandatory surprise:** None. Previous surprise (Rhai HTTP limits) fully resolved.
89 - - **No new code findings.** All previous items remain resolved.
90 -
91 - ### Rhai plugin aggregate timeout (2026-03-22)
92 - - **Test count:** 602 (547 Rust + 55 JS). 0 failures.
93 - - **Rhai Plugins Security:** B -> A. Added 60-second aggregate fetch timeout (`MAX_FETCH_DURATION`, `check_fetch_deadline()`, `AtomicU64` deadline). All 4 original Rhai HTTP security gaps now resolved (request limit, response cap, URL restriction, aggregate timeout).
94 - - **3 new tests:** fetch_deadline_zero_means_no_limit, fetch_deadline_future_passes, fetch_deadline_past_fails
95 - - **Cold spots:** 1 -> 0 (Rhai Plugins Security was the last remaining cold spot)
96 -
97 - ### Tenth audit (2026-03-18, Run 9 cross-project)
98 - - **Test count:** 599 (544 Rust + 55 JS). 0 clippy warnings. 0 failures.
99 - - **Grade:** A (maintained). v0.3.0.
100 - - **Release build:** macOS DMG signed+notarized, verified with codesign + spctl.
101 - - **No new findings.** All previous items remain resolved.
102 - - **Rhai HTTP limits:** Pre-existing gap (no request count limit, no response size cap on `http_get`/`http_get_json`). Accepted at A grade for pre-beta. Roadmap item.
103 - - **Mandatory surprise:** None. Previous surprise (Rhai HTTP limits) unchanged.
104 -
105 - ### Concurrency Upgrade (2026-03-13)
106 - - **Concurrency:** A- -> A
107 - - Replaced std::sync::RwLock/Mutex with parking_lot equivalents (eliminates poison risk). Removed LockPoisoned error variant. Fixed TOCTOU in create_feed with SQLite transaction.
108 -
109 - ### Observability Upgrade (2026-03-13)
110 - - **Observability:** B -> A
111 - - Added 61 `#[instrument(skip_all)]` annotations across 11 files
112 - - Coverage: all 39 Tauri commands (9 command files), 11 orchestrator methods (`bb-core/src/orchestrator.rs`), 11 sync service functions (`sync_service.rs`)
113 - - `use tracing::instrument;` import added to each file
114 - - `cargo check --workspace` passes clean
115 -
116 - ### Adversarial Test Audit (2026-03-13)
117 -
118 - Targeted adversarial testing phase focused on edge cases and boundary conditions. Test count: 520 → 536 (+16 tests). All findings resolved:
119 -
120 - **Critical:**
121 - - **`truncate()` byte/char mismatch** — Used `text.len()` (bytes) instead of char count, causing incorrect truncation of CJK/emoji text. Fixed to use `.chars().count()` for proper Unicode handling.
122 -
123 - **High:**
124 - - **In-memory filters broke pagination** — `has_more` computed after filtering reduced item count; `sql_had_more` was true but filtered results had fewer items than page size. Fixed by checking `sql_had_more` before in-memory filtering.
125 - - **`parse_timestamp` silent failure** — Fell back to `Utc::now()` on parse errors, causing corrupted items to appear at top of feeds. Fixed to use `DateTime::UNIX_EPOCH` (Jan 1, 1970) as fallback.
126 - - **`feed_tags` sync delete broke on colons** — Tag names containing colons broke delete parsing. Fixed to use UUID length (36 chars) for parsing instead of colon delimiter.
127 - - **RSS items without guid or link collided** — Items lacking both fields got empty string as ID, causing collisions. Fixed to synthesize deterministic hash ID from title+summary+published.
128 -
129 - All fixes include regression tests. Zero new clippy warnings. Clean production code unwrap count maintained (7 total, all startup/static).
130 -
131 - ### Seventh audit (2026-03-16, Run 6 cross-project)
132 - - **Test count:** 536 -> 599 (544 Rust + 55 JS)
133 - - **Grade:** A (maintained).
134 - - **New finding (MEDIUM):** 111 `.unwrap()` calls in `bb-core/src/rhai_plugin/conversions.rs` — production Rhai Dynamic-to-Rust conversion that could crash the app if a plugin returns unexpected types. Should use `.try_cast()` or error returns.
135 - - **Mandatory surprise:** conversions.rs unwraps — Genuine issue. The rest of BB has only 7 production unwraps (all startup/static).
136 - - **Previous items verified:** All previous remediated items confirmed intact.
137 -
138 - ### Sixth audit (2026-03-13) -- pre-launch skeptical lens
139 -
140 - Fresh audit. 520 tests verified via `cargo test --workspace -- --list`. Clippy clean. 7 unwrap/expect in non-test production code (all safe: startup, static regex, graceful fallbacks).
141 -
142 - New findings:
143 - 1. **sync_disconnect is a no-op** — returns Ok(true) without disconnecting. Sync session remains active, scheduler keeps running.
144 - 2. **OPML import doesn't validate URL schemes** — unlike feed creation which checks http(s)://, OPML import stores raw xmlUrl values. Caught at fetch time by Rhai layer but bad URLs persist in DB.
145 - 3. **HTML sanitizer missing `<base>` tag** — add to DANGEROUS_ELEMENTS in utils.js.
146 -
147 - Documentation upgraded: QueryCondition fields documented with valid values, 19 error variants documented across 3 enums, stale fetch_plugin doc fixed, RhaiPlugin + ReaderResult field docs added, architecture.md created, README features section added.
148 -
149 - ### Fifth audit (2026-03-11) -- full fresh audit
150 -
151 - Fresh audit of entire codebase per audit.md. Test count: 359 (was 320). Clippy clean (0 warnings). 4 unwrap/expect in non-test production code (all startup/static regex -- acceptable).
152 -
153 - ### Growth since fourth audit
154 -
155 - - **Rust source LOC**: ~5,400 -> ~7,600 (+2,200 lines). Primary additions: sync_service.rs (833 lines), sync_scheduler.rs, settings-sync commands, migration 007.
156 - - **JS LOC**: 1,658 -> 2,140 (+482 lines). Primary addition: settings-sync.js (400 lines).
157 - - **Test count**: 320 -> 359 (+39 tests). 15 sync tests, additional integration tests.
158 - - **External deps**: unchanged at 26 (synckit-client is a workspace path dependency).
159 -
160 - ### New findings (this audit)
161 -
162 - 1. **Rhai HTTP host functions with no limits** -- no request count cap, no response size cap, no URL scheme restriction, no aggregate timeout. Genuine security gap. (Mandatory surprise)
163 - 2. **No circuit breaker** -- consecutive_failures tracked but no auto-disable threshold.
164 - 3. **Sync changelog unbounded growth** -- no retention cap when sync is disconnected.
165 - 4. **FTS query injection edge cases** -- NEAR, column: prefix not covered by sanitize_fts_query.
166 - 5. **Zero JS tests** -- persists from prior audits, now covering 2,140 lines across 12 files.
167 - 6. **Sync polling UX uncertainty** -- OAuth callback polling has comment trail suggesting unreliable flow.
168 - 7. **HN plugin N+1** -- 31+ HTTP calls per fetch, no parallelism within Rhai.
169 -
170 - ### Grades changed from fourth audit
171 -
172 - - Code Quality: A -> A (unchanged; clean clippy maintained)
173 - - Testing: A- (unchanged; +39 tests but JS testing gap persists)
174 - - Performance: A- (unchanged)
175 - - Documentation: A- -> B+ (architecture overview doc still missing; plugin authoring guide exists but not linked from app)
176 - - Frontend: A- -> B+ (2,140 lines now, still zero tests; sync UI adds complexity)
177 - - Observability: new dimension, graded B (tracing configured but no structured spans)
178 - - Concurrency: new dimension, graded A- (tokio async, AbortHandles, sync backoff)
179 - - Resilience: new dimension, graded A- (health tracking, stale cleanup, but no circuit breaker)
180 - - Type Safety: A (unchanged; newtypes solid)
181 - - API Consistency: A (unchanged)
182 - - Codebase Size: A (unchanged; growth is proportional to new features)
183 -
184 - ### Verification
185 -
186 - - `cargo clippy --workspace`: PASS (0 warnings)
187 - - `cargo test --workspace`: PASS (359 tests, 0 failures)
188 - - Zero `.unwrap()` in production business logic
189 - - 4 `.expect()` in production code (2 startup, 1 entry point, 1 static regex -- all acceptable)
190 -
191 - ### JS Audit Remediation (2026-03-11) — Complete (8/8)
192 -
193 - All JS audit findings resolved:
194 - - **Critical (1):** escapeAttr() on item.id in onclick handler
195 - - **Medium (4):** Stale OAuth polling loop removed, updateReadState/updateStarState deduplicated, escapeHtml() on timeAgo/score, detail.js state desync fixed (onItemsChanged subscriber)
196 - - **Low (3):** Unused variables removed (pendingAuth, total), prompt() replaced with BB.ui.openFormModal() for tag editing, inline styles replaced with 6 CSS classes
197 -
198 - ### Audit Grade Corrections (2026-03-13)
199 -
200 - Corrected stale grades where the auditor missed existing code:
201 - - **Resilience:** A- → A. Circuit breaker implemented (migration 008, `CIRCUIT_BREAKER_THRESHOLD=10`, skip in auto-fetch, reset API, event emission). Changelog cap at 10,000 entries with `enforce_changelog_retention()` running on sync tick.
202 - - **Documentation:** A- → A. architecture.md (211 lines), plugin_authoring.md (324 lines), README (74 lines) all complete.
203 - - **Frontend:** B+ → A-. JS test infrastructure added (28 tests covering state, utils, sources, items modules).
204 - - **Overall:** A- → A. All dimensions now A- or above.
205 -
206 - ### Security Deep Dive (2026-03-13) — Complete (2/2)
207 -
208 - - **HTML sanitization:** Added `ammonia = "4"` to `bb-core/Cargo.toml`; `orchestrator.rs` applies `ammonia::clean()` on feed content body after URL tracking stripping and before DB upsert, preventing XSS from untrusted feed sources
209 - - **Search query length limit:** `repository.rs` — `MAX_SEARCH_QUERY_LENGTH = 500` constant with early return in `list_search()` on overlong queries, preventing FTS5 DoS
210 -
211 - ### Still open (from prior audits)
212 -
213 - - ~~Add JS tests (state.js, search debounce, keyboard navigation, sync settings)~~ -- 55 JS tests added
214 - - Consider parallel plugin fetching for scale
81 + See [audit_history.md](./audit_history.md) for full chronological audit log.
215 82
216 83 ---
217 84
@@ -294,7 +161,7 @@
294 161
295 162 ### Overall Doc Grade: A
296 163
297 - Clean doc set. README and PLUGIN_AUTHORING.md are good additions from recent audit work. Main issue was stale test count in todo.md (106 vs 225 actual). description.md is an intentional placeholder.
164 + Clean doc set. README and plugin_authoring.md are good additions from recent audit work. Main issue was stale test count in todo.md (106 vs 225 actual). description.md is an intentional placeholder.
298 165
299 166 ### Document Heatmap
300 167
M docs/todo.md +58 -3
@@ -1,7 +1,9 @@
1 1 # Balanced Breakfast TODO
2 2
3 3 ## Status
4 - Done: All pre-beta phases (Tauri conversion, Phases 1-6, localStorage migration, Rhai safety hardening, FTS sanitization, circuit breaker, sync retention cap, JS audit 8/8, Phase 5 query feeds + reader view, frontend audit 35/35, 7 new plugins). 547 Rust + 55 JS = 602 tests. Clippy clean. Theme audit complete. 10 plugins total. Rhai plugin HTTP sandbox complete (request limit, response cap, URL restriction, 60s aggregate timeout). Zero audit cold spots.
4 + Done: All pre-beta phases, UX polish (Phase 8). Active: None. Next: Post-beta features.
5 +
6 + v0.3.0. Audit grade A. 10 bundled plugins. Rhai sandbox hardened.
5 7
6 8 **Scope:** Pre-beta coding complete. All remaining sections are post-beta.
7 9
@@ -9,6 +11,44 @@
9 11
10 12 ---
11 13
14 + ## Phase 8: UX Polish
15 +
16 + Addresses non-STRONG grades from the UX audit (`_meta/uxaudit.md`, 2026-04-04).
17 +
18 + ### 8A: Active User — Sources Sidebar Empty State
19 + First-time users see an empty sidebar with no guidance.
20 +
21 + - [x] In `sources.js` `render()`, detect when `sources.length === 0` and show onboarding message
22 + - [x] Message: cooking pot emoji + "Add your first feed to get started" with pointer to Add Feed button
23 + - [x] Remove onboarding when first source is added (reactive via `BB.state.subscribe('sources')`)
24 +
25 + ### 8B: Goal-Gradient — Positive Unread Completion State
26 + When a source reaches 0 unread, show a positive visual cue instead of just removing the count.
27 +
28 + - [x] In `sources.js`, when `source.unreadCount === 0` and `source.totalCount > 0`, render checkmark icon
29 + - [x] CSS: `.source-count.all-read` with checkmark glyph + muted green color
30 + - [x] "All" aggregate: when `totalUnread === 0`, show checkmark on the All row too
31 +
32 + ### 8C: Von Restorff — Stronger Selected-Item Highlight
33 + Currently selected item uses `background-color: var(--bg-tertiary)` which is subtle.
34 +
35 + - [x] Add left border accent to `.item.selected`: `border-left-color: var(--yolk)`
36 + - [x] Ensure selected + unread combination is visually distinct (darker accent background)
37 +
38 + ### 8D: Peak-End Rule — Session Summary
39 + Give users a sense of accomplishment about their reading session.
40 +
41 + - [x] Track session stats in `BB.state`: `sessionArticlesRead`, `sessionArticlesStarred`
42 + - [x] On app close (`beforeunload`), show brief toast: "This session: N read, M starred"
43 + - [x] Reset counters on app launch (initialized to 0 in state defaults)
44 +
45 + ### 8E: Fitts's Law — Wider Star Toggle Target
46 + Star button is icon-only (~18px) with no padding.
47 +
48 + - [x] Expand `.star-btn` padding: `0.25rem 0.4rem` with negative margin to maintain layout
49 +
50 + ---
51 +
12 52 ## Phase 7: Plugin OAuth (Post-beta)
13 53
14 54 Authenticated plugin feeds — users log into services via OAuth to access personalized content.
@@ -43,13 +83,28 @@
43 83 - [ ] Saved queries: unify GO saved views, BB query feeds, AF smart folders into shared pattern
44 84 - [ ] FTS5 query building: extract shared SQLite full-text search utilities
45 85
86 + ## Mobile Port (Tauri 2, pre-beta)
87 +
88 + - [ ] Phase 1: CSS foundation (single-column layout, header hidden, safe areas, bottom sheets)
89 + - [ ] Phase 2: Bottom tab bar (Feed / Saved / Sources / + / More)
90 + - [ ] Phase 3: Detail view navigation (full-screen reader, back button, slide transitions)
91 + - [ ] Phase 4: Touch module (swipe actions, pull-to-refresh, long-press, drag-to-dismiss)
92 + - [ ] Phase 5: View adaptations (items, sources, detail, keyboard gating)
93 + - [ ] Phase 6: Tauri mobile build config (dep gating, capabilities, iOS/Android init)
94 + - [ ] Phase 7: Polish (physical devices, accessibility, performance)
95 + - See [todo_mobile.md](./todo_mobile.md) for full mobile breakdown
96 +
97 + ## Desktop Distribution
98 +
99 + ### Remaining
100 + - [ ] Windows build: `cargo tauri build` on Windows, test `.msi`, code-sign
101 + - [ ] Linux build: AppImage (x86_64 + aarch64), .deb
102 +
46 103 ## Deferred
47 104 - [ ] Podcast/media enclosure plugin
48 105 - [ ] Advanced filtering (date ranges, tags, content type)
49 106 - [ ] Read history / analytics
50 107 - [ ] Plugin sandboxing / security model
51 - - [ ] Android build: `cargo tauri android init`, icon assets, Play Store listing
52 - - [ ] Windows build: `cargo tauri build` on Windows, test `.msi`, code-sign
53 108 - [ ] Replace native confirm() with custom styled modal for feed deletion
54 109
55 110 ## Key Paths
@@ -75,7 +75,7 @@
75 75 // Fetch papers for each category
76 76 let items = [];
77 77 for category in categories {
78 - let cat_items = fetch_category(category, max_results);
78 + let cat_items = fetch_category(category, max_results, ARXIV_API);
79 79 for item in cat_items {
80 80 items.push(item);
81 81 }
@@ -87,11 +87,11 @@
87 87 }
88 88 }
89 89
90 - fn fetch_category(category, max_results) {
90 + fn fetch_category(category, max_results, api_base) {
91 91 let items = [];
92 92
93 93 // Build the arXiv API URL
94 - let url = ARXIV_API + "?search_query=cat:" + category + "&sortBy=submittedDate&sortOrder=descending&max_results=" + max_results;
94 + let url = api_base + "?search_query=cat:" + category + "&sortBy=submittedDate&sortOrder=descending&max_results=" + max_results;
95 95
96 96 // Fetch the feed
97 97 let xml = http_get(url);
@@ -165,7 +165,11 @@
165 165 content: #{
166 166 title: title,
167 167 body: abstract_text,
168 - url: link
168 + url: link,
169 + actions: [
170 + #{ label: "View PDF", action_type: "download", url: "https://arxiv.org/pdf/" + paper_id },
171 + #{ label: "ar5iv HTML", action_type: "open", url: "https://ar5iv.labs.arxiv.org/html/" + paper_id }
172 + ]
169 173 },
170 174 meta: #{
171 175 source_name: "arXiv " + category,
@@ -43,10 +43,18 @@
43 43 }
44 44
45 45 fn fetch(config, cursor) {
46 - // Get story type from config
46 + // Validate story type
47 47 let story_type = "top";
48 48 if config.story_type != () {
49 49 story_type = config.story_type;
50 + let valid = ["top", "new", "best", "ask", "show", "job"];
51 + let found = false;
52 + for v in valid {
53 + if story_type == v { found = true; }
54 + }
55 + if !found {
56 + throw_config_error("Invalid story type: " + story_type);
57 + }
50 58 }
51 59
52 60 // Get limit from config
@@ -87,7 +95,7 @@
87 95 // Fetch each story
88 96 let items = [];
89 97 for story_id in ids {
90 - let item = fetch_story(story_id, story_type);
98 + let item = fetch_story(story_id, story_type, HN_API);
91 99 if item != () {
92 100 items.push(item);
93 101 }
@@ -118,8 +126,8 @@
118 126 "topstories"
119 127 }
120 128
121 - fn fetch_story(story_id, story_type) {
122 - let url = HN_API + "/item/" + story_id + ".json";
129 + fn fetch_story(story_id, story_type, api_base) {
130 + let url = api_base + "/item/" + story_id + ".json";
123 131 let item = http_get_json(url);
124 132
125 133 if item == () {
@@ -2,9 +2,19 @@
2 2 //
3 3 // Uses the `extract_article` host function (readability algorithm)
4 4 // to strip navigation, ads, and other clutter.
5 + //
6 + // Special cases:
7 + // arXiv — rewrites arxiv.org/abs/ to ar5iv HTML rendering for full paper view.
5 8
6 9 fn extract(url) {
7 - let html = http_get(url);
10 + let fetch_url = url;
11 +
12 + // arXiv: use ar5iv for rendered HTML instead of the abstract page
13 + if str_contains(url, "arxiv.org/abs/") {
14 + fetch_url = str_replace(url, "arxiv.org/abs/", "ar5iv.labs.arxiv.org/html/");
15 + }
16 +
17 + let html = http_get(fetch_url);
8 18 let article = extract_article(html);
9 19 article
10 20 }
@@ -47,6 +47,10 @@
47 47 }
48 48 }
49 49
50 + if feed_urls.is_empty() {
51 + throw_config_error("No feed URL configured");
52 + }
53 +
50 54 // Fetch each feed
51 55 for feed_url in feed_urls {
52 56 let feed_items = fetch_feed(feed_url);
@@ -82,7 +82,7 @@
82 82 }
83 83
84 84 if comic != () {
85 - let item = parse_comic(comic);
85 + let item = parse_comic(comic, XKCD_URL);
86 86 if item != () {
87 87 items.push(item);
88 88 fetched += 1;
@@ -105,7 +105,7 @@
105 105 result
106 106 }
107 107
108 - fn parse_comic(comic) {
108 + fn parse_comic(comic, base_url) {
109 109 if comic.title == () {
110 110 return ();
111 111 }
@@ -123,7 +123,7 @@
123 123 img = comic.img;
124 124 }
125 125
126 - let url = XKCD_URL + "/" + num + "/";
126 + let url = base_url + "/" + num + "/";
127 127
128 128 // Build a simple HTML body with the comic image and alt text
129 129 let body = "";
A CHANGELOG.md +27