Skip to main content

max / balanced_breakfast

13.7 KB · 300 lines History Blame Raw
1 # Balanced Breakfast Frontend Architecture
2
3 Vanilla JavaScript frontend for the Tauri 2 desktop feed aggregator. No framework, no build step, no bundler. 14 source files organized under the `BB` global namespace.
4
5 ## Namespace
6
7 All code lives under `window.BB`. No other globals. Cross-module calls use `BB.moduleName.functionName()`.
8
9 ```
10 BB
11 .api Tauri IPC abstraction (api.js)
12 .state Proxy-based reactive state with pub/sub (state.js)
13 .ui Toast, modal, form builder, progress bar, confirm dialog (components.js)
14 .utils HTML escaping, sanitization, debounce (utils.js)
15 .sources Sources sidebar, tag filtering, feed edit/delete (sources.js)
16 .items Items list, selection, read/star toggle, pagination (items.js)
17 .detail Detail panel, reader view, save-to-file (detail.js)
18 .feeds Add feed (plugin picker + schema form), refresh, OPML import/export (feeds.js)
19 .queryFeeds Query feed CRUD, condition builder modal (query-feeds.js)
20 .themes Theme loading, CSS variable derivation, selector UI (themes.js)
21 .sync SyncKit cloud sync settings UI (settings-sync.js)
22 .updater OTA update banner (updater.js)
23 .app Bootstrap, keyboard shortcuts, menu listeners, settings (app.js)
24 ```
25
26 ## Module Inventory
27
28 | Module | File | Purpose |
29 |--------|------|---------|
30 | Namespace | `bb.js` | Creates `window.BB` with empty sub-namespace slots |
31 | State | `state.js` | Proxy-wrapped reactive store with `subscribe()`, `set()`, `get()`, `resetPagination()` |
32 | API | `api.js` | Thin `invoke()` wrappers grouped by domain (sources, items, plugins, feeds, opml, queryFeeds, reader, actions, sync) |
33 | Utils | `utils.js` | `escapeHtml`, `escapeAttr`, `sanitizeHtml`, `debounce`, `getErrorMessage` |
34 | Components | `components.js` | `showToast`, `showProgress`, `openModal`, `closeModal`, `openFormModal`, `showErrorWithRetry`, `confirmAction` |
35 | Query Feeds | `query-feeds.js` | CRUD for saved filter rules, condition builder modal with field/operator/value rows |
36 | Sources | `sources.js` | Sidebar rendering, source selection, tag bar, feed popover (health/edit/delete), feed edit/tags forms |
37 | Items | `items.js` | Item list rendering, selection, star/read toggle with in-flight guards, pagination, saved-articles view |
38 | Detail | `detail.js` | Detail panel rendering, reader view extraction, save-to-file as HTML, plugin action buttons, saved-items localStorage |
39 | Feeds | `feeds.js` | Two-step add-feed flow (plugin picker then schema form), refresh with progress bar, OPML import/export, plugin warning |
40 | Settings/Sync | `settings-sync.js` | 4-state sync UI (connect, authenticating, encryption, ready), API key flow, sync event listener |
41 | Themes | `themes.js` | Load/apply themes via Tauri IPC, derive CSS variables from theme colors, theme selector `<select>` builder |
42 | Updater | `updater.js` | OTA update banner with consent (Install & Restart / Not Now), listens for `update-available` and `menu:check_updates` events |
43 | App | `app.js` | `init()` bootstrap, keyboard shortcuts (vim-style j/k), search debounce, sort select, native menu listeners, settings/help/welcome modals |
44
45 ## Module Pattern
46
47 Every module is an IIFE that registers its public API on the namespace:
48
49 ```javascript
50 (function() {
51 'use strict';
52
53 // Private constants and helpers
54 const ITEMS_PER_PAGE = 50;
55 function privateHelper() { ... }
56
57 // Public functions
58 async function load() { ... }
59 function render(items) { ... }
60
61 // Register on namespace
62 BB.myModule = { load, render };
63 })();
64 ```
65
66 Rules:
67 - All modules use `'use strict'`
68 - Private state stays inside the IIFE closure (e.g., `currentItem` in detail.js, `inFlight` set in items.js)
69 - Public API is the object assigned to `BB.moduleName`
70 - No `window.X` exports besides `window.BB`
71 - Prefer `async/await` over `.then()` chains
72
73 ## State Management
74
75 `BB.state` is a `Proxy` over a plain data object. Property reads go through the proxy `get` trap; property writes trigger `set()` which notifies subscribers.
76
77 ### Reading state
78
79 ```javascript
80 const items = BB.state.items;
81 const source = BB.state.currentSource;
82 // or explicit:
83 const page = BB.state.get('currentPage');
84 ```
85
86 ### Writing state
87
88 ```javascript
89 BB.state.set('items', updatedItems); // Triggers subscribers
90 BB.state.currentSource = 'my-feed'; // Also triggers (via Proxy set trap)
91 ```
92
93 ### Subscribing to changes
94
95 ```javascript
96 const unsub = BB.state.subscribe('items', (newItems, oldItems) => {
97 render(newItems);
98 });
99 ```
100
101 ### State properties
102
103 | Property | Type | Domain |
104 |----------|------|--------|
105 | `sources` | Array | Data -- feed sources with counts |
106 | `items` | Array | Data -- feed items for current view |
107 | `queryFeeds` | Array | Data -- saved filter rules |
108 | `allTags` | Array | Data -- distinct tags across all feeds |
109 | `currentSource` | string | Filter -- busser_id or `''` for all, `'__saved__'` for saved |
110 | `currentOrder` | string | Filter -- `'chronological'`, `'score'`, `'unread'`, `'starred'` |
111 | `currentSearch` | string | Filter -- search query text |
112 | `currentTag` | string | Filter -- tag filter or `''` for all |
113 | `currentQueryFeed` | string/null | Filter -- active query feed ID |
114 | `currentPage` | number | Pagination -- zero-indexed page |
115 | `hasMore` | boolean | Pagination -- backend has more pages |
116 | `selectedItemId` | string/null | Selection -- active item in list |
117 | `selectedPluginId` | string/null | UI -- plugin chosen in add-feed flow |
118
119 `resetPagination(clearSelection?)` is a convenience method that sets `currentPage` to 0 and optionally clears `selectedItemId`.
120
121 ## Three-Panel Layout Data Flow
122
123 The UI has three columns: sources sidebar (left), items list (center), detail panel (right).
124
125 ```
126 User clicks source in sidebar
127 -> BB.sources.select(sourceId)
128 -> BB.state.set('currentSource', sourceId)
129 -> BB.state.resetPagination(true)
130 -> BB.items.load()
131 -> BB.api.items.list(filter) // IPC to Rust
132 -> Rust filters + paginates + returns
133 -> BB.state.set('items', data.items) // triggers subscriber
134 -> BB.items.render(items) // subscriber re-renders list
135 ```
136
137 ```
138 User clicks item in list
139 -> BB.items.selectItem(id)
140 -> BB.state.set('selectedItemId', id)
141 -> BB.detail.load(id)
142 -> BB.api.items.get(id) // IPC to Rust
143 -> BB.detail.renderDetail(item) // renders detail panel
144 -> BB.api.items.markRead(id) // background, then reload
145 ```
146
147 ### Reactive subscriptions
148
149 Two state subscriptions drive automatic re-rendering:
150 - `BB.state.subscribe('items', render)` in items.js -- re-renders item list on any items change
151 - `BB.state.subscribe('sources', render)` in sources.js -- re-renders sidebar on source list change
152 - `BB.state.subscribe('items', onItemsChanged)` in detail.js -- merges updated summary fields into the detail view
153
154 ## API Layer
155
156 `BB.api` wraps every Tauri IPC command. The UI never calls `__TAURI__.core.invoke` directly (except themes.js and app.js which use `invoke` for config get/set).
157
158 ```javascript
159 const sources = await BB.api.sources.list();
160 const data = await BB.api.items.list({ source: 'my-feed', page: 0 });
161 await BB.api.items.star(itemId);
162 await BB.api.feeds.create({ busserId: 'rss', name: 'My Feed', config: { url: '...' } });
163 ```
164
165 API groups:
166
167 | Group | Commands |
168 |-------|----------|
169 | `sources` | `list` |
170 | `items` | `list`, `get`, `markRead`, `markUnread`, `star`, `unstar`, `unreadCount` |
171 | `plugins` | `list`, `schema` |
172 | `feeds` | `getByBusser`, `get`, `create`, `update`, `delete`, `deleteByBusser`, `fetchAll`, `setTags`, `listAllTags`, `resetCircuitBreaker` |
173 | `opml` | `export`, `import` |
174 | `queryFeeds` | `list`, `create`, `update`, `delete` |
175 | `reader` | `extract` |
176 | `actions` | `downloadAndOpen` |
177 | `sync` | `testApiKey`, `saveApiKey`, `status`, `startAuth`, `completeAuth`, `disconnect`, `now`, `setupEncryptionNew`, `setupEncryptionExisting`, `updateSettings` |
178
179 ## Form Modal System
180
181 `BB.ui.openFormModal()` builds a form from a field specification array and handles submit/cancel:
182
183 ```javascript
184 BB.ui.openFormModal({
185 title: 'Add RSS Feed',
186 fields: [
187 { name: 'name', type: 'text', label: 'Feed Name', required: true },
188 { name: 'url', type: 'text', label: 'URL', required: true, placeholder: 'https://...' },
189 ],
190 submitLabel: 'Add Feed',
191 onSubmit: async (data) => {
192 await BB.api.feeds.create({ busserId: 'rss', name: data.name, config: { url: data.url } });
193 BB.ui.showToast('Feed created!');
194 },
195 });
196 ```
197
198 Supported field types: `text`, `textarea`, `select`, `secret` (renders as password), plus any HTML input type.
199
200 The two-step add-feed flow in feeds.js uses this system: step 1 shows a plugin picker list, step 2 calls `BB.api.plugins.schema(pluginId)` to get the plugin's config fields and passes them to `openFormModal()`.
201
202 ## Load Order
203
204 Scripts load in order via `<script>` tags in `index.html`:
205
206 1. `bb.js` -- creates `window.BB` namespace with empty sub-objects
207 2. `themes.js` -- populates `BB.themes` (init, load, buildSelector)
208 3. `utils.js` -- populates `BB.utils` (escapeHtml, escapeAttr, sanitizeHtml, debounce, getErrorMessage)
209 4. `state.js` -- replaces `BB.state` with Proxy-wrapped reactive store
210 5. `api.js` -- replaces `BB.api` with invoke wrappers
211 6. `components.js` -- replaces `BB.ui` with toast/modal/form/confirm functions
212 7. `query-feeds.js` -- populates `BB.queryFeeds` (depends on utils, state, api, ui)
213 8. `sources.js` -- populates `BB.sources` (depends on utils, state, api, ui, queryFeeds)
214 9. `items.js` -- populates `BB.items` (depends on utils, state, api, ui, detail)
215 10. `detail.js` -- populates `BB.detail` (depends on utils, state, api, ui)
216 11. `feeds.js` -- populates `BB.feeds` (depends on utils, state, api, ui, sources, items)
217 12. `settings-sync.js` -- populates `BB.sync` (depends on api, ui)
218 13. `updater.js` -- populates `BB.updater` (depends on ui, utils)
219 14. `app.js` -- populates `BB.app`, auto-calls `init()` on DOMContentLoaded
220
221 Note: items.js (9) references `BB.detail` at call time (not load time), so the forward reference to detail.js (10) resolves by the time `init()` runs.
222
223 ## Key Utility Functions
224
225 ```javascript
226 BB.utils.escapeHtml(str) // XSS prevention for innerHTML
227 BB.utils.escapeAttr(str) // Attribute-safe escaping
228 BB.utils.sanitizeHtml(html) // DOM-based sanitizer for rich content (strips script, iframe, on* attrs, javascript: URLs)
229 BB.utils.debounce(fn, delay) // Delay execution until after last call
230 BB.utils.getErrorMessage(err) // Extract string from Tauri ApiError or any error shape
231 ```
232
233 `sanitizeHtml` is specific to BB (not present in GO). It uses a detached DOM element to parse HTML, then walks the tree removing dangerous elements (`script`, `iframe`, `object`, `embed`, `form`, `style`, `base`) and dangerous attributes (`on*` handlers, `javascript:`/`data:`/`vbscript:` URLs). Used by detail.js to render feed item bodies.
234
235 ## Tauri Event Listeners
236
237 Several modules subscribe to Tauri events beyond IPC commands:
238
239 | Event | Listener | Purpose |
240 |-------|----------|---------|
241 | `fetch-progress` | feeds.js | Per-plugin progress during refresh |
242 | `auto-fetch-complete` | app.js | Background auto-fetch finished, reload sources+items |
243 | `auto-fetch-error` | app.js | Background fetch error, show toast |
244 | `feed-circuit-broken` | app.js | Feed disabled after repeated failures, show retry toast |
245 | `menu:refresh` | app.js | Native menu bar Refresh action |
246 | `menu:add_feed` | app.js | Native menu bar Add Feed |
247 | `menu:import_opml` | app.js | Native menu bar Import OPML |
248 | `menu:export_opml` | app.js | Native menu bar Export OPML |
249 | `menu:view_all` | app.js | Native menu bar View All |
250 | `menu:view_unread` | app.js | Native menu bar View Unread |
251 | `menu:view_starred` | app.js | Native menu bar View Starred |
252 | `menu:check_updates` | updater.js | Native menu bar Check for Updates |
253 | `update-available` | updater.js | OTA update detected by Rust backend |
254 | `sync:changes-applied` | settings-sync.js | SyncKit pulled remote changes, reload sources+items |
255
256 ## CSS
257
258 Single stylesheet at `frontend/css/styles.css`. Uses CSS variables set by themes.js at runtime. Theme colors come from shared TOML files in `MNW/shared/themes/` loaded via `get_theme` Tauri command.
259
260 Theme-derived CSS variables:
261
262 | Variable | Source |
263 |----------|--------|
264 | `--bg-primary` | `background.primary` |
265 | `--bg-secondary` | `background.secondary` |
266 | `--bg-tertiary` | `background.tertiary` |
267 | `--text-primary` | `foreground.primary` |
268 | `--text-secondary` | `foreground.secondary` |
269 | `--text-muted` | `foreground.muted` |
270 | `--accent` | `accent.red` |
271 | `--accent-hover` | `accent.red` lightened 10% |
272 | `--yolk` | `accent.yellow` |
273 | `--yolk-light` | `accent.yellow` lightened 15% |
274 | `--border` | `border.default` |
275 | `--border-dark` | `border.default` darkened 10% |
276 | `--shadow` | `foreground.primary` at 8% opacity |
277 | `--shadow-hover` | `foreground.primary` at 12% opacity |
278 | `--success` | `accent.green` |
279
280 ## Key Paths
281
282 | What | Where |
283 |------|-------|
284 | Namespace root | `src-tauri/frontend/js/bb.js` |
285 | State manager | `src-tauri/frontend/js/state.js` |
286 | API layer | `src-tauri/frontend/js/api.js` |
287 | UI components | `src-tauri/frontend/js/components.js` |
288 | Items list | `src-tauri/frontend/js/items.js` |
289 | Detail panel | `src-tauri/frontend/js/detail.js` |
290 | Sources sidebar | `src-tauri/frontend/js/sources.js` |
291 | Feed management | `src-tauri/frontend/js/feeds.js` |
292 | Query feeds | `src-tauri/frontend/js/query-feeds.js` |
293 | Theme system | `src-tauri/frontend/js/themes.js` |
294 | Sync settings | `src-tauri/frontend/js/settings-sync.js` |
295 | OTA updater | `src-tauri/frontend/js/updater.js` |
296 | App bootstrap | `src-tauri/frontend/js/app.js` |
297 | Styles | `src-tauri/frontend/css/styles.css` |
298 | Entry point | `src-tauri/frontend/index.html` |
299 | JS tests | `src-tauri/frontend/js/tests/` |
300