max / balanced_breakfast
- Co-Authored-By
- Claude Opus 4.6 <noreply@anthropic.com>
77 files changed,
+8223 insertions,
-4740 deletions
| @@ -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]] |
| @@ -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 |
| @@ -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 = ""; |