Skip to main content

max / balanced_breakfast

Update path deps and resource paths for ~/Code directory layout Move project docs into repo. Update Cargo.toml path dependencies and tauri.conf.json theme resource glob for new Shared/ location. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-30 02:26 UTC
Commit: 2a421da2e5d35bde3fee1fd8953ca6c5e8ce3c31
Parent: 42051ce
7 files changed, +610 insertions, -13 deletions
M Cargo.toml +3 -3
@@ -44,7 +44,7 @@ parking_lot = "0.12"
44 44 # Utilities
45 45 chrono = { version = "0.4.43", features = ["serde"] }
46 46 uuid = { version = "1.20.0", features = ["v4", "serde"] }
47 - theme-common = { path = "../theme-common" }
47 + theme-common = { path = "../../Shared/theme-common" }
48 48 toml = "0.8.2"
49 49 dirs = "6.0.0"
50 50
@@ -53,7 +53,7 @@ bb-interface = { path = "crates/bb-interface" }
53 53 bb-core = { path = "crates/bb-core" }
54 54 bb-feed = { path = "crates/bb-feed" }
55 55 bb-db = { path = "crates/bb-db" }
56 - synckit-client = { path = "../synckit-client" }
56 + synckit-client = { path = "../../Shared/synckit-client" }
57 57 tauri = "2.10.2"
58 58 tauri-plugin-updater = "2"
59 - tagtree = { path = "../tagtree" }
59 + tagtree = { path = "../../Shared/tagtree" }
@@ -46,7 +46,7 @@ url = "2"
46 46 regex = "1"
47 47
48 48 # HTML sanitization for untrusted feed content
49 - docengine = { path = "../../../docengine" }
49 + docengine = { path = "../../../../Shared/docengine" }
50 50
51 51 # Article extraction (reader view)
52 52 readable-readability = "0.4"
@@ -0,0 +1,216 @@
1 + # Balanced Breakfast -- Architecture
2 +
3 + When it comes to media and news, it's good to be a picky eater.
4 +
5 + ## Positioning
6 +
7 + The only native desktop feed reader with a user-scriptable plugin system for arbitrary sources. Rhai plugins let users write custom source adapters (no other reader is extensible this way). First-class Hacker News and arXiv support. Free with no limits, no account required, local-first. Cross-platform native (macOS, Windows, Linux via Tauri 2). Target users: power users/developers consuming content from many sources, privacy-conscious local-first users, technical professionals who value keyboard shortcuts and hackability.
8 +
9 + ## System Overview
10 +
11 + Balanced Breakfast is a desktop feed aggregator built with Tauri 2. It unifies RSS, Atom, JSON Feed, Hacker News, arXiv, and other sources into a single timeline. The backend is a Rust workspace with four library crates and one application crate. The frontend is vanilla HTML/CSS/JS served by Tauri's webview. Feeds are fetched by Rhai script plugins ("bussers"), stored in SQLite, and presented through a three-panel layout (sources, items, detail).
12 +
13 + ## Workspace Layout
14 +
15 + | Crate | Path | Purpose |
16 + |-------|------|---------|
17 + | bb-interface | `crates/bb-interface/` | Leaf crate. Shared types for the plugin contract: `FeedItem`, `FetchResult`, `ConfigSchema`, `ConfigField`, `BusserCapabilities`, `BusserConfig`. No internal dependencies. |
18 + | bb-core | `crates/bb-core/` | Orchestrator and plugin runtime. Coordinates plugins, database, and feed scheduling. Contains the Rhai engine setup, plugin manager, config encryption (`crypto`), and URL tracker stripping (`url_cleaner`). Depends on bb-interface. |
19 + | bb-feed | `crates/bb-feed/` | Feed aggregation and ordering. `FeedGenerator` reads items from the DB and applies filters (source, unread, starred, search, tags, query feed conditions). `OrderBy` sorts results (chronological, score, unread-first, starred-first). Depends on bb-interface. |
20 + | bb-db | `crates/bb-db/` | SQLite persistence via sqlx. Repository types for feeds, items, tags, busser state, user config, and query feeds. FTS5 full-text search. Depends on bb-interface. |
21 + | src-tauri | `src-tauri/` | Tauri 2 desktop shell. Thin command wrappers over the library crates, app state management, background tasks (auto-fetch, stale cleanup), sync scheduler, and the vanilla JS frontend. Depends on all four library crates. |
22 +
23 + Dependency flow: `bb-interface` (leaf) --> `bb-core`, `bb-feed`, `bb-db` --> `src-tauri` (root).
24 +
25 + ## Orchestrator
26 +
27 + The `Orchestrator` (`bb-core::orchestrator`) is the central coordination point. It owns the `Database` and a `PluginManager` behind an `Arc<RwLock<>>`. Its responsibilities:
28 +
29 + - **Plugin lifecycle** -- load `.rhai` scripts from the plugins directory, initialize them with config from the DB, and provide fetch/shutdown operations.
30 + - **Fetch execution** -- call a plugin's `fetch()`, strip tracking parameters from item URLs and HTML bodies, upsert results into the DB via the items repository, and record success/failure on the feed.
31 + - **Circuit breaker** -- after 10 consecutive fetch failures (`CIRCUIT_BREAKER_THRESHOLD`), the feed is marked `circuit_broken` and excluded from auto-fetch until manually reset.
32 + - **Secret management** -- holds an optional AES-256-GCM key. On startup, encrypts any plaintext Secret fields in existing feed configs (migration from legacy plaintext).
33 + - **Fetch-all** -- iterates all loaded plugins and fetches each, collecting total item counts.
34 +
35 + The orchestrator does not own the fetch scheduler or background tasks. Those are managed by `AppState` in the Tauri layer.
36 +
37 + ## Plugin System (Rhai)
38 +
39 + Plugins are `.rhai` text files dropped into the plugins directory. The Rhai engine is configured with safety limits and host functions.
40 +
41 + ### Plugin Contract
42 +
43 + Every plugin must define four functions:
44 +
45 + - `id()` -- returns a unique string identifier (e.g. `"rss"`, `"hackernews"`)
46 + - `name()` -- returns a human-readable display name
47 + - `config_schema()` -- returns a map describing configuration fields (key, label, field_type, required, default, options)
48 + - `fetch(config, cursor)` -- returns `{ items: [...], has_more: bool, next_cursor: string? }`
49 +
50 + An optional `capabilities()` function can declare pagination support, custom fetch intervals, auth requirements, etc.
51 +
52 + ### Sandboxing
53 +
54 + - **Operations cap:** `max_operations(100_000)` -- a typical RSS fetch costs 1k-5k ops; this catches infinite loops.
55 + - **Expression depth:** `max_expr_depths(128, 128)` -- prevents stack overflows from deeply nested or recursive scripts.
56 + - **HTTP timeout:** 15 seconds per request.
57 + - **Response size:** 2 MB cap per response body.
58 + - **Request limit:** 100 HTTP requests per `fetch()` invocation. Counter resets at the start of each fetch.
59 + - **URL validation:** only `http://` and `https://` schemes; localhost, `127.0.0.1`, `[::1]`, `0.0.0.0`, and private RFC 1918 ranges (`10.x`, `172.16-31.x`, `192.168.x`, `169.254.x`) are blocked.
60 +
61 + ### Host Functions
62 +
63 + Functions registered into the Rhai engine for scripts to call:
64 +
65 + | Function | Description |
66 + |----------|-------------|
67 + | `http_get(url)` | Fetch URL, return response body as string |
68 + | `http_get_json(url)` | Fetch URL, parse JSON, return as Dynamic |
69 + | `parse_json(str)` | Parse a JSON string |
70 + | `parse_xml(str)` | Parse XML into a simplified `{tag, text, attrs, children}` structure |
71 + | `parse_feed(str)` | Auto-detect RSS/Atom/JSON Feed, return `{title, link, entries}` |
72 + | `parse_datetime(str)` | Parse ISO 8601 or RFC 2822 date to Unix timestamp |
73 + | `timestamp_now()` | Current UTC timestamp (seconds) |
74 + | `html_to_text(html)` | Strip HTML, render as plain text (80-char width) |
75 + | `extract_article(html)` | Readability extraction: returns `{title, content, text}` |
76 + | `truncate(text, max)` | Truncate with ellipsis |
77 + | `str_contains`, `str_split`, `str_replace`, `str_trim` | String utilities |
78 + | `strip_tracking(url)` | Remove utm_*, fbclid, gclid, etc. from a URL |
79 + | `parse_int(str)` | Parse string to integer (returns UNIT on failure) |
80 + | `debug_print(val)` | Log to tracing at debug level |
81 +
82 + ### Config Field Types
83 +
84 + Plugins declare their configuration schema with these field types: `Text`, `TextArea`, `Secret`, `Url`, `Number`, `Toggle`, `Select`. Fields marked `Url` become feed subscriptions; other fields become key-value options passed to `fetch()`.
85 +
86 + ### Bundled Plugins
87 +
88 + Three plugins ship with the app: `rss.rhai` (RSS/Atom/JSON Feed), `hackernews.rhai` (HN stories), `arxiv.rhai` (arXiv papers). A `reader.rhai` plugin extracts article content from URLs using the readability algorithm.
89 +
90 + ## Feed Aggregation
91 +
92 + The `FeedGenerator` (`bb-feed::generator`) reads items from the database, applies filters and ordering, and returns paginated results.
93 +
94 + **Filtering** combines SQL-level and in-memory strategies:
95 + - Source, unread, starred, and FTS5 search are pushed into SQL for accurate LIMIT/OFFSET pagination.
96 + - Item-level tags, feed-level tags, and query feed conditions (title/author/body contains, equals, not_contains, matches_regex) run in-memory after the SQL query.
97 +
98 + **Ordering** is applied in-memory after filtering:
99 + - `Chronological` -- newest first (default)
100 + - `Score` -- highest score first, with chronological tiebreak
101 + - `UnreadFirst` -- unread items before read, chronological within each group
102 + - `StarredFirst` -- starred items before unstarred, chronological within each group
103 +
104 + **Pagination** fetches `page_size + 1` items to detect whether more pages exist, then truncates to the exact page size.
105 +
106 + ## Database Layer
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.
109 +
110 + ### Tables
111 +
112 + | Table | Purpose |
113 + |-------|---------|
114 + | `feeds` | Registered feed subscriptions. Keyed by UUID, linked to a busser_id. Tracks config JSON, enabled state, last_fetch, health counters, and circuit breaker state. |
115 + | `feed_items` | All fetched items. Deduplicated by `external_id` (UNIQUE). Stores bite display fields, full content, metadata (score, tags as JSON array), and user state (is_read, is_starred). |
116 + | `feed_items_fts` | FTS5 virtual table in external-content mode. Indexes title, body, and bite_text. Kept in sync via INSERT/UPDATE/DELETE triggers. |
117 + | `feed_tags` | User-assigned tags on feeds (many-to-many). |
118 + | `busser_state` | Plugin key-value state (cursors, tokens, pagination markers). Keyed by `(busser_id, key)`. |
119 + | `user_config` | Key-value preferences (theme, welcome flag). Synced via changelog triggers. |
120 + | `query_feeds` | Saved filter rules that act as virtual sources. Rules stored as JSON array. Synced via changelog triggers. |
121 + | `sync_state` | Single-row sync metadata (device_id, pull_cursor, auto_sync settings). |
122 + | `sync_changelog` | Local changes pending push. Written by triggers on feeds, feed_tags, user_config, query_feeds, and feed_items (user state only). |
123 +
124 + ### Repositories
125 +
126 + - `FeedsRepository` -- CRUD, enable/disable, last_fetch updates, fetch failure recording, circuit breaker management
127 + - `ItemsRepository` -- upsert (dedup by external_id), read/star toggling, paginated listing (by busser, by feed, unread, starred), FTS5 search, counts, stale item deletion
128 + - `TagsRepository` -- per-feed tag assignment, distinct tag listing, bulk feed-tag pairs
129 + - `StateRepository` -- busser key-value state (get/set/delete by busser_id + key)
130 + - `ConfigRepository` -- user_config key-value pairs (get/set/delete)
131 + - `QueryFeedsRepository` -- query feed CRUD (create/update/delete/list)
132 +
133 + FTS5 queries are sanitized by wrapping each search term in double quotes to prevent syntax injection (`AND`, `OR`, `NOT`, `NEAR` operators). The `^` prefix and `*` suffix characters are stripped.
134 +
135 + ## Sync Integration
136 +
137 + Balanced Breakfast integrates with the SyncKit client SDK for cross-device sync. The `sync_service` module handles push/pull of local changes.
138 +
139 + **What gets synced:**
140 + - Feed subscriptions (feeds table: config, enabled state, health counters)
141 + - Feed tags
142 + - User config (preferences)
143 + - Query feeds (saved filter rules)
144 + - Feed item user state (is_read, is_starred changes only -- not item content)
145 +
146 + **How it works:** SQLite triggers on synced tables write changes to `sync_changelog`. The sync engine pushes unpushed entries in batches of 500, pulls remote changes using a cursor, and applies them in FK-safe order (parents before children for upserts, children before parents for deletes). A `applying_remote` flag in `sync_state` suppresses trigger firing during remote change application to prevent echo loops.
147 +
148 + The sync scheduler runs on a configurable interval (default 15 minutes). Encryption is E2E via the SyncKit client's ChaCha20-Poly1305 with keys stored in the OS keychain.
149 +
150 + ## Security Model
151 +
152 + - **Plugin secrets at rest:** AES-256-GCM encryption. Encrypted format: `bb_enc:v1:<base64(nonce[12] || ciphertext || tag[16])>`. Key stored in `encryption.key` with 0600 permissions (Unix). Backward-compatible: unencrypted values pass through on decrypt.
153 + - **FTS5 query sanitization:** User search input is quoted per-word to prevent FTS5 operator injection. Special characters (`^`, `*`) are stripped.
154 + - **URL validation:** Rhai HTTP host functions block non-HTTP schemes and requests to localhost/internal addresses.
155 + - **Response size limits:** 2 MB cap on HTTP response bodies prevents memory exhaustion.
156 + - **URL tracking removal:** utm_*, fbclid, gclid, msclkid, and other tracking parameters stripped from item URLs and body HTML on ingest.
157 + - **Sync encryption:** E2E via SyncKit (ChaCha20-Poly1305 + Argon2 key derivation). Server never sees plaintext.
158 +
159 + ## Concurrency Model
160 +
161 + - **Tokio async runtime** (multi-threaded) drives all I/O: database queries, HTTP fetches, sync operations.
162 + - **`Arc<RwLock<PluginManager>>`** -- the orchestrator holds the plugin manager behind a Tokio RwLock. Read lock for fetches and schema queries; write lock only during plugin loading.
163 + - **`Arc<AppState>`** -- shared across Tauri commands and background tasks. Managed by Tauri's state system.
164 + - **AbortHandles** -- background tasks (auto-fetch loop, stale cleanup) store their `AbortHandle` in `AppState` behind `std::sync::Mutex`. On shutdown or task replacement, existing handles are aborted.
165 + - **Auto-fetch loop** -- checks every 60 seconds which plugins are due for a fetch based on their last_fetch timestamp and configured interval.
166 + - **Stale cleanup** -- runs every 6 hours, deleting read (non-starred) items older than 30 days.
167 +
168 + ## Frontend Architecture
169 +
170 + The frontend is vanilla HTML/CSS/JS served by Tauri's webview. There is no build step or bundler.
171 +
172 + - **Tauri commands** act as thin wrappers: each command extracts parameters, calls the orchestrator or feed generator, and returns a serialized response. All business logic lives in the library crates.
173 + - **Tauri events** notify the frontend of background activity: `auto-fetch-complete` (new items available), `auto-fetch-error`, `feed-circuit-broken`.
174 + - **JS files** live in `src-tauri/frontend/js/`. Communication with Rust is via `window.__TAURI__`.
175 +
176 + ## Feed Health Tracking
177 +
178 + Each feed tracks `consecutive_failures` and `last_error`. On fetch success, failures reset to 0. On failure, the counter increments. Health status:
179 +
180 + - **Green (healthy):** 0 consecutive failures
181 + - **Yellow (degraded):** 1-9 consecutive failures
182 + - **Red (circuit broken):** 10+ failures trips the circuit breaker; feed is excluded from auto-fetch until manually reset via `reset_circuit_breaker`
183 +
184 + ## Key Design Decisions
185 +
186 + - **Rhai over WASM/Lua:** Rhai is a Rust-native scripting language with easy type bridging and built-in safety limits. No FFI boundary, no separate runtime. Plugins are plain text files, not compiled artifacts.
187 + - **Single orchestrator:** All coordination flows through one struct. No message passing between crates; the orchestrator calls methods directly. Simpler than an actor model for this scale.
188 + - **SQL-first filtering with in-memory fallback:** Simple filters (source, unread, starred, search) use SQL for correct pagination. Complex filters (regex, tag intersection, query feed conditions) run in-memory. This avoids dynamic SQL generation while keeping common paths fast.
189 + - **External-content FTS5:** The FTS index references `feed_items` by rowid with no data duplication. Triggers keep it in sync. This saves disk space compared to a full-copy FTS table.
190 + - **Dedup by external_id:** Items use `external_id` (UNIQUE) for deduplication on upsert. The busser provides the ID; the DB enforces uniqueness.
191 + - **Changelog-based sync:** SQLite triggers write changes to `sync_changelog` rather than diffing snapshots. This captures intent (INSERT/UPDATE/DELETE) and works naturally with the SyncKit push/pull model.
192 +
193 + ## Key Paths
194 +
195 + | What | Where |
196 + |------|-------|
197 + | Workspace manifest | `Cargo.toml` |
198 + | Plugin interface types | `crates/bb-interface/src/` |
199 + | Orchestrator | `crates/bb-core/src/orchestrator.rs` |
200 + | Plugin manager | `crates/bb-core/src/plugin_manager.rs` |
201 + | Rhai runtime | `crates/bb-core/src/rhai_plugin/` |
202 + | Host functions | `crates/bb-core/src/rhai_plugin/host_functions.rs` |
203 + | Type conversions | `crates/bb-core/src/rhai_plugin/conversions.rs` |
204 + | Secret encryption | `crates/bb-core/src/crypto.rs` |
205 + | URL cleaner | `crates/bb-core/src/url_cleaner.rs` |
206 + | Feed generator | `crates/bb-feed/src/generator.rs` |
207 + | Ordering/filtering | `crates/bb-feed/src/ordering.rs` |
208 + | Database layer | `crates/bb-db/src/` |
209 + | Repositories | `crates/bb-db/src/repository.rs` |
210 + | Migrations | `migrations/sqlite/` (001-009) |
211 + | Tauri app state | `src-tauri/src/state.rs` |
212 + | Tauri commands | `src-tauri/src/commands/` |
213 + | Sync service | `src-tauri/src/sync_service.rs` |
214 + | Bundled plugins | `plugins/` |
215 + | Frontend JS | `src-tauri/frontend/js/` |
216 + | Frontend CSS | `src-tauri/frontend/css/` |
@@ -0,0 +1,320 @@
1 + # Balanced Breakfast -- Code Audit Review
2 +
3 + **Last audited:** 2026-03-28 (eleventh audit, Run 12 cross-project)
4 + **Previous audit:** 2026-03-18 (tenth audit, Run 9 cross-project)
5 +
6 + ## Overall Grade: A
7 +
8 + Run 12 cross-project audit. 602 tests (547 Rust + 55 JS). 0 clippy warnings. v0.3.0. Grade stable at A. No code changes since Run 9. New dependency advisories: rustls-webpki (RUSTSEC-2026-0049), tar x2 (RUSTSEC-2026-0067, -0068).
9 +
10 + ## Scorecard
11 +
12 + | Dimension | Grade | Notes |
13 + |-----------|:-----:|-------|
14 + | Code Quality | A | Near-zero unwraps in production code (4 total, all startup/static). Clean clippy. Consistent `thiserror` + `Result` error handling throughout. |
15 + | Architecture | A | Excellent 4-crate separation with clear dependency flow: interface <- core <- db, feed. Tauri layer is a thin shell. Sync service well-isolated. |
16 + | Testing | A | 547 Rust tests + 55 JS tests, 0 failures. Inline tests in every module + 50 integration tests. JS test infrastructure covers state, utils, sources, items, settings-sync. |
17 + | Security | A | AES-256-GCM encryption at rest, Rhai engine sandboxing (100k ops, depth 128, 32 call levels), input validation, XSS sanitization, PKCE OAuth2, path traversal protection. Rhai HTTP sandbox: 100 requests/fetch, 2 MB response cap, URL scheme + private range blocking, 60s aggregate timeout. ammonia HTML sanitization on all feed content, FTS5 search query length limit (500 chars). |
18 + | Performance | A | Proper indexes, FTS5, pagination, debounced search, theme caching, LazyLock statics. SqlitePool max_connections=16 (appropriate for WAL mode concurrent readers). |
19 + | Documentation | A | architecture.md (211 lines), plugin_authoring.md (324 lines), README (74 lines). Good JSDoc and Rust doc comments. Plugin trust model documented. Repository methods documented. |
20 + | Dependencies | A- | 26 direct external deps -- reasonable for a Tauri app with crypto, DB, XML, theming. All well-maintained crates. Workspace-level dep management. |
21 + | Frontend | A | Good UX patterns (skeleton loading, undo, keyboard shortcuts, themes). Vanilla JS keeps it simple. 55 automated JS tests covering state, utils, sources, items, and settings-sync state machine. Gap: no TypeScript. |
22 + | Type Safety | A | Newtype UUIDs (FeedId, ItemId, BusserStateId, BusserId) via macro, typed errors (ApiError, OrchestratorError, BusserError), exhaustive enums, strong Rhai<->Rust conversion boundaries. |
23 + | Observability | A | Tracing subscriber configured. 61 `#[instrument(skip_all)]` annotations across all Tauri commands + 11 orchestrator methods + 11 sync service functions. Auto-fetch errors emit Tauri events. Feed health tracking. |
24 + | Concurrency | A | Tokio async throughout, parking_lot RwLock/Mutex (no poison risk), AbortHandle-based task cancellation, exponential backoff on sync (max 15 min). TOCTOU in create_feed fixed with SQLite transaction. |
25 + | Resilience | A | Per-feed error isolation, health indicators (green/yellow/red), stale item cleanup, sync retry with backoff. Circuit breaker implemented (migration 008, threshold 10, skip in auto-fetch, reset API, event emission). Changelog cap at 10,000 entries with `enforce_changelog_retention()` on sync tick. |
26 + | API Consistency | A | Uniform command patterns, consistent error serialization (5 error codes), builder patterns throughout. Unified pagination with page_size+1 has_more detection. |
27 + | Codebase Size | A | ~7,600 lines production Rust + 2,140 lines JS for 20+ features. Right-sized for its scope. No files violate the 500-line branching guideline. |
28 +
29 + ## Module Heatmap
30 +
31 + | Module | Code | Arch | Test | Security | Perf | Docs | Deps | Frontend |
32 + |--------|:----:|:----:|:----:|:--------:|:----:|:----:|:----:|:--------:|
33 + | bb-interface (~380 lines) | A | A | A | n/a | n/a | A | A | n/a |
34 + | bb-core (~1,700 lines) | A | A | A- | A | A- | A | n/a | n/a |
35 + | bb-db (~1,100 lines) | A | A | A | A | A- | A | n/a | n/a |
36 + | bb-feed (~500 lines) | A | A- | A | n/a | A- | A | n/a | n/a |
37 + | src-tauri (~3,900 lines) | A- | A | A- | A | A | A | n/a | n/a |
38 + | JS Frontend (2,140 lines) | A- | B+ | A- | A- | B+ | A- | n/a | A |
39 + | Rhai Plugins (~580 lines) | B+ | A- | A | A | n/a | A- | n/a | n/a |
40 + | SQL Migrations (250 lines) | A | A- | n/a | n/a | A | A- | n/a | n/a |
41 +
42 + ### Cold Spots
43 +
44 + - ~~**JS Frontend (Testing): D**~~ -- 55 automated JS tests covering state management, utilities, sources sidebar, items list, and settings-sync 4-state flow. Upgraded D -> A-.
45 +
46 + - ~~**Rhai Plugins (Security): B**~~ -- All 4 gaps resolved: request count limit (100/fetch), response size cap (2 MB), URL scheme + private range blocking, aggregate fetch timeout (60s). Upgraded B -> A.
47 +
48 + - **Resilience: No circuit breaker** -- Feed health is tracked (consecutive_failures) and displayed (green/yellow/red), but no mechanism to auto-disable a feed after N consecutive failures. A dead feed retries every 60 seconds forever.
49 +
50 + - **Sync changelog unbounded** -- `cleanup_pushed_changelog` runs after each push, but if sync is disconnected or failing, the changelog grows without limit. No retention cap.
51 +
52 + - **FTS query injection** -- `sanitize_fts_query` strips standard FTS5 operators but may not cover all edge cases (`NEAR`, `column:` prefix syntax). Low risk (local SQLite DB) but worth hardening.
53 +
54 + ## Mandatory Surprise
55 +
56 + **[RESOLVED] The Rhai plugin runtime had no HTTP request limits or timeout enforcement.**
57 +
58 + All four gaps have been fixed in `host_functions.rs`:
59 +
60 + - **Request count limit:** `MAX_REQUESTS_PER_FETCH = 100` with `check_request_limit()` on every HTTP call.
61 + - **Response size cap:** `MAX_RESPONSE_BYTES = 2 MB` via `.take()` on response reader.
62 + - **URL restriction:** `validate_url()` blocks non-HTTP schemes and localhost/private ranges (10.x, 192.168.x, 172.16-31.x, 169.254.x, [::1], 0.0.0.0).
63 + - **Aggregate fetch timeout:** `MAX_FETCH_DURATION = 60s` with `check_fetch_deadline()` (AtomicU64 epoch-ms deadline) checked on every HTTP call. Prevents 100 x 15s = 25 min worst case.
64 +
65 + **Verdict: Resolved.** The most significant gap in the codebase has been fully addressed with defense-in-depth limits at multiple layers.
66 +
67 + ## Metrics Over Time
68 +
69 + | Metric | Audit 1 (2026-02-27) | Audit 2 (2026-02-28) | Audit 3 (2026-02-28) | Audit 4 (2026-03-01) | Audit 5 (2026-03-11) | Audit 6 (2026-03-13) | Adversarial (2026-03-13) | 2026-03-22 |
70 + |--------|:----:|:----:|:----:|:----:|:----:|:----:|:----:|:----:|
71 + | Overall Grade | A | A- | A- | A- | A- | A- | A- | A | A |
72 + | Tests | 178 | 225 | 225 | 315 | 359 | 520 | 536 | 599 | 602 |
73 + | Clippy warnings | 0 | 0 | 8 | 1 | 0 | 0 | 0 | 0 | 0 |
74 + | Unwrap/expect (prod) | 6 | 3 | 3 | 3 | 4 | 7 | 7 | 7+111 | 7+111 |
75 + | Rust source LOC | ~5,400 | ~5,400 | ~5,400 | ~5,400 | ~7,600 | ~7,600 | ~7,600 | ~7,600 | ~7,600 |
76 + | JS LOC | 1,658 | 1,658 | 1,658 | 1,658 | 2,140 | 2,140 | 2,140 | 2,140 | 2,140 |
77 + | Cold spots | 5 | 5 | 2 | 8 | 5 | 4 | 4 | 1 | 0 |
78 +
79 + ## Changes Since Last Audit
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
215 +
216 + ---
217 +
218 + ## Strengths
219 +
220 + - **Zero `.unwrap()` in business logic.** Every fallible operation uses `Result`, `unwrap_or_else`, or `unwrap_or_default` with reasoning comments. Only 4 unwrap/expect in startup/static contexts.
221 +
222 + - **All SQL is parameterized.** Every query in `repository.rs` (1,822 lines) uses `?N` placeholders with `.bind()`. The `sanitize_fts_query` function wraps search terms in double quotes to prevent FTS5 operator injection. Dynamic placeholder strings built safely.
223 +
224 + - **Defense-in-depth security.** Multiple independent layers: Rhai engine sandboxed (100k ops, depth 128), AES-256-GCM encryption for secrets at rest with versioned format, HTML sanitization strips dangerous elements, XML escaping in OPML export, URL tracker parameter stripping, extensive input validation.
225 +
226 + - **Clean crate boundaries.** `bb-interface` defines types/traits with zero impl deps. `bb-db` handles persistence with no plugin knowledge. `bb-core` orchestrates without touching SQL. `bb-feed` handles aggregation. No circular dependencies.
227 +
228 + - **Comprehensive test suite (602 tests).** Coverage spans crypto roundtrips, FTS5 search, feed health, tag CRUD, stale cleanup, upsert conflict, plugin manager lifecycle, Rhai conversions (50+ tests), ordering (25 tests), generator pagination, command integration (50 tests), sync service.
229 +
230 + - **Well-designed sync integration.** FK-safe ordering on remote changes, table column whitelists preventing arbitrary data injection, `applying_remote` flag prevents changelog loops, exponential backoff with 15-minute cap.
231 +
232 + ## Weaknesses
233 +
234 + - ~~**Rhai HTTP functions with no limits.**~~ All 4 gaps resolved: request count limit (100/fetch), response size cap (2 MB), URL scheme + private range blocking, aggregate fetch timeout (60s).
235 +
236 + - ~~**No circuit breaker.**~~ Circuit breaker implemented (migration 008, threshold 10, skip in auto-fetch, reset API, event emission).
237 +
238 + - ~~**Sync changelog unbounded growth.**~~ Changelog cap at 10,000 entries with `enforce_changelog_retention()` on sync tick.
239 +
240 + - ~~**Zero automated JS tests.**~~ 55 JS tests covering state management, utilities, sources sidebar rendering, items rendering, and settings-sync 4-state flow.
241 +
242 + - **FTS query sanitization gaps.** `sanitize_fts_query` strips standard operators but may not cover `NEAR` or `column:` prefix syntax. Low risk (local DB) but incomplete.
243 +
244 + ## Competitive Comparison
245 +
246 + Based on `docs/apps/bb/competition.md`, BB holds a unique position as the only native desktop feed reader with a user-scriptable plugin system.
247 +
248 + **Gaps closed since the competition analysis was written:**
249 + - Full-text search: Implemented via FTS5
250 + - Tags/categories: Feed tags with junction table, sidebar filter bar
251 + - URL tracker stripping: Implemented in `url_cleaner.rs`
252 + - JSON Feed format: Implemented in `conversions.rs` (1.0/1.1)
253 + - Feed config validation: Schema-aware validation in `create_feed`
254 + - Secret encryption: AES-256-GCM with versioned format
255 + - Feed health monitoring: consecutive_failures tracking, health dots
256 + - Stale item cleanup: Background task every 6h
257 + - Theming: 9 TOML themes, bundled + user custom
258 + - Cloud sync: SyncKit integration with E2E encryption
259 +
260 + **Remaining competitive gaps (from competition.md priority list):**
261 + 1. Reader view / full-article fetch -- planned as a plugin (Phase 5)
262 + 2. Filter/query feeds (virtual feeds from rules) -- Phase 5
263 + 3. Mobile support -- deferred (Tauri mobile)
264 +
265 + ## Action Items
266 +
267 + All resolved from prior audits. New items from fifth audit filed in `docs/apps/bb/todo.md`.
268 +
269 + ### Resolved (prior audits)
270 + - ~~Fix single-quote XSS in tag filter~~ -- sources.js uses addEventListener
271 + - ~~Set 0600 permissions on encryption.key~~ -- crypto.rs sets 0o600
272 + - ~~Replace `.expect("poisoned")`~~ -- replaced with error propagation
273 + - ~~Remove deprecated health stubs~~ -- markFailed/clearFailed/clearAllFailed removed
274 + - ~~Add `data:` URI blocking~~ -- utils.js blocks both javascript: and data:
275 + - ~~bb-feed generator tests~~ -- tag filtering tests added
276 + - ~~Tauri command integration tests~~ -- 50 tests added
277 + - ~~README with setup instructions~~ -- README.md at project root
278 + - ~~Plugin authoring guide~~ -- included in README
279 + - ~~Fix 8 clippy violations~~ -- all resolved
280 + - ~~Fix `.env` PostgreSQL URL~~ -- changed to sqlite
281 + - ~~Fix clippy warning (unused OrderBy import)~~ -- removed
282 + - ~~Add repository doc comments~~ -- 46+ methods documented
283 + - ~~Unify pagination strategy~~ -- standardized page_size+1
284 + - ~~Add background task unit tests~~ -- any_feed_due extracted + tested
285 + - ~~Add search loading indicator~~ -- CSS spinner added
286 + - ~~Entity ID newtypes~~ -- FeedId, ItemId, BusserStateId, BusserId
287 + - ~~ApiErrorCode enum~~ -- replaces String error codes
288 +
289 + ---
290 +
291 + ## Documentation Review
292 +
293 + **Last reviewed:** 2026-03-04 (first doc audit)
294 +
295 + ### Overall Doc Grade: A
296 +
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.
298 +
299 + ### Document Heatmap
300 +
301 + | Document | Status | Last Verified | Notes |
302 + |----------|:------:|:-------------:|-------|
303 + | docs/apps/bb/todo.md | Current | 2026-03-11 | Updated with audit 5 action items |
304 + | docs/apps/bb/todo_done.md | Current | 2026-03-04 | Completed items archive |
305 + | docs/apps/bb/description.md | Placeholder | 2026-03-04 | Intentional placeholder |
306 + | docs/apps/bb/competition.md | Current | 2026-03-04 | Competitive analysis |
307 + | docs/apps/bb/structural_metrics.md | New | 2026-03-11 | Structural metrics from audit 5 |
308 + | docs/apps/bb/stress_test.md | New | 2026-03-11 | Phase 4/5 stress test |
309 + | README.md | Current | 2026-03-04 | Setup instructions |
310 +
311 + ### Stale References Found (Doc Audit)
312 +
313 + | Location | Issue | Resolution |
314 + |----------|-------|------------|
315 + | docs/apps/bb/todo.md | Status line says "106 tests passing" -- actual is 225 | Updated to "225 tests passing" (audit 4) |
316 + | docs/apps/bb/todo.md | Status line updated to 359 tests | Updated (audit 5) |
317 +
318 + ### Doc Action Items
319 +
320 + - None critical. Test count kept in sync.
@@ -239,17 +239,10 @@ Each field in the `fields` array supports these properties:
239 239
240 240 The Rhai engine enforces these limits per plugin execution:
241 241
242 - **Script limits:**
243 242 - **Max operations:** 100,000 -- Caps total operations per script call. A typical RSS fetch uses 1,000-5,000 operations. This limit catches infinite loops while allowing complex plugins.
244 243 - **Max expression depth:** 128 -- Limits AST nesting depth for both expressions and function calls, preventing stack overflows from deeply recursive scripts.
245 244
246 - **HTTP limits (per `fetch()` call):**
247 - - **Request count:** 100 -- Maximum HTTP requests per fetch invocation. Counter resets before each `fetch()` call. A typical RSS plugin makes 1-3 requests; 100 allows pagination while catching runaways.
248 - - **Response size:** 2 MB -- Each HTTP response body is capped at 2 MB. Prevents a plugin from consuming unbounded memory on a large or malicious response.
249 - - **Request timeout:** 15 seconds -- Per-request timeout for `http_get` and `http_get_json`.
250 - - **URL restrictions:** Only `http://` and `https://` schemes are allowed. Requests to localhost, private networks (10.x, 172.16-31.x, 192.168.x, 169.254.x), and IPv6 loopback (::1) are blocked.
251 -
252 - If a plugin exceeds any limit, the engine terminates execution and returns an error.
245 + If a plugin exceeds either limit, the engine terminates execution and returns an error.
253 246
254 247 ## Complete Minimal Example
255 248
A docs/todo.md +68
@@ -0,0 +1,68 @@
1 + # Balanced Breakfast TODO
2 +
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.
5 +
6 + **Scope:** Pre-beta coding complete. All remaining sections are post-beta.
7 +
8 + Completed work archived in `docs/archive/bb_todo_done.md`.
9 +
10 + ---
11 +
12 + ## Phase 7: Plugin OAuth (Post-beta)
13 +
14 + Authenticated plugin feeds — users log into services via OAuth to access personalized content.
15 +
16 + ### 7A: OAuth Infrastructure
17 + - [ ] New host functions: `oauth_start(provider, scopes)`, `oauth_token(provider)`, `oauth_refresh(provider)`
18 + - [ ] Tauri deep-link handler for OAuth callback URLs
19 + - [ ] Token store (per-plugin, per-account) with encryption at rest
20 + - [ ] Token refresh logic (automatic before expiry)
21 +
22 + ### 7B: Authenticated Plugins (priority order)
23 + - [ ] Mastodon — home timeline, notifications, bookmarks (standard OAuth2, any instance)
24 + - [ ] Reddit — personalized feed, saved posts, private subreddits
25 + - [ ] GitHub — notifications, private repos, org feeds
26 + - [ ] YouTube — subscriptions feed, watch later
27 + - [ ] Bluesky — home feed, notifications (AT Protocol app passwords)
28 + - [ ] StackExchange — inbox, followed questions
29 + - [ ] Dev.to — reading list, followed tags
30 +
31 + ## Phase 4: Plugin Store (Long-term)
32 +
33 + Community plugin marketplace. Prerequisites: plugin sandboxing, plugin authoring guide.
34 + - [ ] 4A: Plugin registry backend (manifest format, index, submission flow, CI validation)
35 + - [ ] 4B: Client-side store UI (browse, search, install, update, uninstall)
36 + - [ ] 4C: Publishing from the app
37 + - [ ] 4D: Trust & safety (review queue, permission declarations, abuse reporting, revocation)
38 +
39 + ## Shared Code Extraction (Cross-Project)
40 + - [ ] Updater UI: extract nearly-identical updater.js from GO/BB into shared module
41 + - [ ] Theme loading: deduplicate TOML theme parser across GO/BB/AF
42 + - [ ] Rhai host functions: deduplicate plugin runtime setup across GO/BB/AF
43 + - [ ] Saved queries: unify GO saved views, BB query feeds, AF smart folders into shared pattern
44 + - [ ] FTS5 query building: extract shared SQLite full-text search utilities
45 +
46 + ## Deferred
47 + - [ ] Podcast/media enclosure plugin
48 + - [ ] Advanced filtering (date ranges, tags, content type)
49 + - [ ] Read history / analytics
50 + - [ ] 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 + - [ ] Replace native confirm() with custom styled modal for feed deletion
54 +
55 + ## Key Paths
56 + ```
57 + Cargo.toml Workspace config
58 + src-tauri/src/ Tauri app (main, lib, state, commands/)
59 + src-tauri/frontend/ HTML + JS (BB namespace) + CSS
60 + crates/bb-core/src/orchestrator.rs Main coordinator
61 + crates/bb-core/src/plugin_manager.rs Plugin loading
62 + crates/bb-core/src/rhai_plugin.rs Rhai script execution
63 + crates/bb-interface/src/ Plugin trait & types
64 + crates/bb-db/src/ Database (lib, repository, models)
65 + crates/bb-feed/src/ Aggregation & filtering
66 + plugins/ 10 Rhai plugins
67 + migrations/sqlite/ SQLite migrations
68 + ```
@@ -29,7 +29,7 @@
29 29 "targets": "all",
30 30 "resources": [
31 31 "../plugins/*.rhai",
32 - "../../../themes/*.toml"
32 + "../../../Shared/themes/*.toml"
33 33 ],
34 34 "icon": [
35 35 "icons/32x32.png",