Skip to main content

max / goingson

Rewrite architecture.md for the app that exists The same defect 3c5dae72 fixed in design-system.md and styleguide.md, in the one doc that task's done condition did not name. Eight measured lines described the deleted SPA: the GoingsOn.* namespace, the state pub/sub, VirtualScroller, themes.js, index.html as the entry point, and JS tests under frontend/js/tests/. Not a banner and not a delete. Unlike frontend_architecture.md, most of this file is about the domain crates and is still true, so the frontend sections are rewritten and the rest is corrected against the tree rather than trusted: - sqlx 0.8 became rusqlite behind an r2d2 pool on 2026-08-07, and the repositories went sync in the same pass. The command example had .await on a call that no longer has one, and returned String where it returns ApiError - two crates became five: go-mcp and got open the same goingson.db as peer readers - 33 migrations, then 50 in the layer below, are 69 - the repository trait list was missing six traits and named two that no longer exist; the model list was missing seven types - Neobrute is Platinum-informed The frontend section is now what the swap actually left: one module per screen under src/quasi/, the five surviving scripts and what each is for, and the six features that went out with index.html. Commands are documented as having two callers, since a described screen calls one as a plain Rust function (67 sites) and only the mobile entry point and host.js reach them over IPC.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-24 16:41 UTC
Signed with PGP, not checked
Commit: 5721a472281a699d71bd94ee27eb144dd7b958ac
Parent: 3845056
1 file changed, +154 insertions, -176 deletions
M docs/architecture.md +154 -176
@@ -2,7 +2,12 @@
2 2
3 3 Email, calendar, tasks in one place. Project management for individuals and small teams.
4 4
5 - A Rust-based productivity application built with Tauri 2 (Rust backend + Vanilla JS frontend), SQLite with sqlx 0.8, and a "Neobrute" design aesthetic. 2 crates: `core` (domain models), `db-sqlite` (repository).
5 + A Rust productivity application on Tauri 2. The screens are described in Rust and
6 + rendered by `quasi-webview`; there is no hand-written frontend. SQLite through
7 + `rusqlite` behind an `r2d2` pool. Platinum-informed look, from the `makeover`
8 + family. Five workspace members: `core` (domain models), `db-sqlite`
9 + (repositories), `go-mcp` (an MCP server over the same file), `got` (a terminal
10 + reader over the same file), and `src-tauri` (the app, and the screens).
6 11
7 12 ## High-Level Overview
8 13
@@ -10,11 +15,14 @@
10 15 ┌─────────────────────────────────────────────────────────┐
11 16 │ User Interface │
12 17 │ ┌────────────────────────────────────────────────────┐ │
13 - │ │ Tauri Desktop (Vanilla JS) │ │
18 + │ │ Described screens (src-tauri/src/quasi/) │ │
19 + │ │ rendered by quasi-webview, served at quasi:// │ │
14 20 │ └─────────────────────┬──────────────────────────────┘ │
15 21 │ │ │
16 22 │ ┌─────────────────────▼──────────────────────────────┐ │
17 - │ │ Tauri Commands (src-tauri/) │ │
23 + │ │ Commands (src-tauri/commands/) │ │
24 + │ │ called as plain functions by the screens, and │ │
25 + │ │ registered for IPC for the few host jobs │ │
18 26 │ └─────────────────────┬──────────────────────────────┘ │
19 27 └────────────────────────┼─────────────────────────────────┘
20 28
@@ -39,18 +47,25 @@
39 47 goingson/
40 48 ├── crates/
41 49 │ ├── core/ # Domain models, traits, business logic
42 - │ └── db-sqlite/ # SQLite repository implementations
43 - ├── src-tauri/ # Tauri desktop app (single-user)
50 + │ ├── db-sqlite/ # SQLite repository implementations
51 + │ ├── go-mcp/ # MCP server over the same goingson.db
52 + │ └── got/ # Terminal cockpit over the same goingson.db
53 + ├── src-tauri/ # Tauri desktop app, and the described screens
44 54 └── migrations/
45 - └── sqlite/ # SQLite schema migrations (33 files)
55 + └── sqlite/ # SQLite schema migrations (69 files)
46 56 ```
47 57
58 + `go-mcp` and `got` open the same SQLite file as peer readers and write through
59 + the normal repository layer, so the sync-changelog triggers fire and a running
60 + desktop app stays consistent.
61 +
48 62 ## Crate Dependencies
49 63
50 64 ```
51 - goingson-desktop (src-tauri)
52 - └── goingson-db-sqlite
53 - └── goingson-core
65 + goingson-desktop (src-tauri) go-mcp got
66 + └──────────────┬─────────────┴────────┘
67 + goingson-db-sqlite
68 + └── goingson-core
54 69 ```
55 70
56 71 ## Core Crate (`crates/core/`)
@@ -61,11 +76,15 @@
61 76
62 77 | Module | Purpose |
63 78 |--------|---------|
64 - | `models/` | Domain types (17 model files) |
65 - | `repository.rs` | Repository traits (data access contracts) |
79 + | `models/` | Domain types (19 model files) |
80 + | `repository/` | Repository traits, one module per area |
66 81 | `urgency.rs` | TaskWarrior-inspired urgency calculation algorithm |
67 - | `parser.rs` | Quick-add natural language parser |
82 + | `parser.rs`, `search_parser.rs`, `date_parser.rs` | Quick-add, search and date parsing |
68 83 | `recurrence.rs` | Task/event recurrence logic |
84 + | `weekly_review.rs`, `monthly_review.rs`, `day_planning.rs` | The review and planning workflows |
85 + | `email_compose.rs`, `email_convert.rs`, `email_sync.rs` | Email domain logic |
86 + | `backup_restore.rs`, `import/` | Backup and import |
87 + | `tz.rs`, `date_utils.rs`, `text_utils.rs` | Shared helpers |
69 88 | `validation.rs` | Input validation trait |
70 89 | `constants.rs` | Named constants for thresholds, formats |
71 90 | `error.rs` | Unified CoreError type |
@@ -76,8 +95,9 @@
76 95 // Domain entities
77 96 Project, Task, Event, Email, EmailAccount, User
78 97 Contact, ContactEmail, ContactPhone, SocialHandle, ContactCustomField
79 - SavedView, Annotation, Subtask, Milestone
80 - WeeklyReview, BackupSettings
98 + SavedView, Annotation, Subtask, Milestone, Attachment
99 + TaskDependency, TimeSession, Context, DailyNote, Problem
100 + WeeklyReview, MonthlyReview, SyncAccount, BackupSettings
81 101
82 102 // Enums with display/parse support
83 103 ProjectType, ProjectStatus, TaskStatus, Priority, Recurrence
@@ -101,36 +121,34 @@
101 121 ProjectRepository, TaskRepository, EventRepository
102 122 EmailRepository, EmailAccountRepository, ContactRepository
103 123 SearchRepository, StatsRepository, SavedViewRepository
104 - AnnotationRepository, SubtaskRepository, MilestoneRepository
124 + MilestoneRepository, AttachmentRepository, ContextRepository
125 + DailyNoteRepository, MonthlyReviewRepository, ProblemRepository
126 + SyncAccountRepository, UserRepository
105 127 WeeklyReviewRepository, BackupSettingsRepository
106 - UserRepository
107 128 ```
108 129
109 130 ## Database Layer (`crates/db-sqlite/`)
110 131
111 - SQLite persistence for the desktop app. Single-user, local storage. 50 migrations in `migrations/sqlite/`.
132 + SQLite persistence, single-user and local. 69 migrations in `migrations/sqlite/`.
133 +
134 + `rusqlite` behind an `r2d2` pool. **The repositories are synchronous**: an async
135 + caller reaches them through `tokio::task::spawn_blocking`. The tree moved off
136 + sqlx-sqlite on 2026-08-07 and the repository traits went sync in the same pass,
137 + because the async had come from sqlx rather than from anything the app wanted.
138 + Wiki: `sql-library-standard`.
112 139
113 140 ```
114 141 src/
115 - ├── lib.rs # SQLite pool initialization
142 + ├── lib.rs # Pool initialization
143 + ├── migrate.rs # Migration runner
144 + ├── migrations.rs # The embedded migration set
116 145 ├── utils.rs # format_datetime, parse_uuid, email validation
117 - └── repository/
118 - ├── mod.rs # Re-exports all repositories
119 - ├── project_repo.rs
120 - ├── task_repo.rs
121 - ├── event_repo.rs
122 - ├── email_repo.rs
123 - ├── email_account_repo.rs
124 - ├── contact_repo.rs
125 - ├── user_repo.rs
126 - ├── search_repo.rs # FTS5 full-text search
127 - ├── stats_repo.rs # Dashboard aggregations
128 - ├── saved_view_repo.rs
129 - ├── annotation_repo.rs
130 - ├── subtask_repo.rs
131 - ├── milestone_repo.rs
132 - ├── weekly_review_repo.rs
133 - └── backup_settings_repo.rs
146 + └── repository/ # One module per aggregate, plus:
147 + ├── mod.rs # Re-exports
148 + ├── restore.rs # Backup restore, which writes across every table
149 + ├── search_repo.rs # FTS5 full-text search
150 + ├── stats_repo.rs # Dashboard aggregations
151 + └── task_repo_state.rs # The task state machine, split off task_repo
134 152 ```
135 153
136 154 ## Tauri Desktop App (`src-tauri/`)
@@ -139,10 +157,14 @@
139 157
140 158 ```
141 159 src/
142 - ├── main.rs # Tauri app setup, command registration
160 + ├── main.rs, lib.rs # Tauri app setup, command registration
143 161 ├── state.rs # AppState with repository instances
162 + ├── quasi/ # The screens, described. This is the frontend
163 + ├── syncstore/ # The SyncKit manifest, blob policy and observer
144 164 ├── notifications.rs # Snooze watcher, native notifications
145 - ├── email/ # IMAP/SMTP client
165 + ├── email/, jmap/ # IMAP/SMTP and JMAP clients
166 + ├── oauth/, external_sync/
167 + ├── export/, problems/
146 168 └── commands/
147 169 ├── mod.rs # Re-exports all commands
148 170 ├── error.rs # Error type definitions
@@ -169,79 +191,85 @@
169 191
170 192 ### Command Pattern
171 193
172 - Tauri commands are async functions that:
173 - 1. Accept `State<Arc<AppState>>` for repository access
174 - 2. Deserialize input from frontend via `#[serde(rename_all = "camelCase")]`
175 - 3. Call repository methods
176 - 4. Serialize response types back to frontend
194 + A command accepts `State<Arc<AppState>>`, calls repository methods, and returns a
195 + response type or an `ApiError`. The repositories are synchronous, so the body has
196 + no `.await` in it even though the command is `async`.
177 197
178 198 ```rust
179 199 #[tauri::command]
180 - pub async fn create_task(
181 - state: State<'_, Arc<AppState>>,
182 - input: TaskInput,
183 - ) -> Result<TaskResponse, String> {
184 - state.tasks
185 - .create(DESKTOP_USER_ID, new_task)
186 - .await
187 - .map(TaskResponse::from)
188 - .map_err(|e| e.to_string())
200 + #[instrument(skip_all)]
201 + pub async fn list_tasks(state: State<'_, Arc<AppState>>) -> Result<Vec<TaskResponse>, ApiError> {
202 + let tasks = state.tasks.list_all(DESKTOP_USER_ID)?;
203 + Ok(tasks.into_iter().map(TaskResponse::from).collect())
189 204 }
190 205 ```
191 206
192 - ## Frontend Architecture (Tauri Desktop)
207 + **Commands have two callers now, and only one of them is IPC.** A described
208 + screen calls the command as a plain Rust function (67 sites), which is why the
209 + command layer survived the swap intact rather than being absorbed into the
210 + screens. The 261 registered commands are still reachable over IPC, which is what
211 + the mobile entry point and the handful of host jobs in `frontend/js/host.js` use.
193 212
194 - The desktop frontend uses vanilla JavaScript organized under the `GoingsOn` global namespace. 66 source files.
213 + ## The Frontend: described screens
195 214
196 - ### Namespace Organization
215 + There is no hand-written frontend. The window opens on `quasi://localhost/tasks`
216 + and there is no other document. `index.html` and the 84 scripts under
217 + `frontend/js/` were deleted on 2026-08-22.
218 +
219 + ### What it was for
220 +
221 + Not porting screens for its own sake. `escape.js` and its 332 `esc()` call sites
222 + existed because screens were built by concatenating strings in JavaScript, and
223 + every one of them had to remember to escape. Escaping is typed in Rust at the
224 + renderer now, so the escapers and the CHRONIC-XSS gate that policed them are both
225 + gone, in that order and not the other one.
226 +
227 + ### The shape
228 +
229 + One module per screen under `src-tauri/src/quasi/`, each contributing its own
230 + routes. There is no `Router::merge`, so composition is a chain of functions that
231 + each take the router and give it back, rather than a central table that has to be
232 + kept in step with the modules.
197 233
198 234 ```
199 - window.GoingsOn = {
200 - api: { ... }, // Tauri IPC abstraction layer
201 - state: { ... }, // Centralized state with pub/sub
202 - ui: { ... }, // Modal, toast, form utilities
203 - utils: { ... }, // HTML escaping, validation
204 -
205 - // Domain modules (IIFE-wrapped)
206 - projects: { ... },
207 - tasks: { ... },
208 - events: { ... },
209 - emails: { ... },
210 - contacts: { ... },
211 -
212 - // Feature modules
213 - savedViews: { ... },
214 - snooze: { ... },
215 - navigation: { ... },
216 - settings: { ... },
217 - app: { ... },
218 -
219 - // Infrastructure
220 - VirtualScroller, // Virtual scrolling for large lists
221 - SelectionManager, // Multi-select with shift/ctrl
222 - PaginationManager, // Page navigation
223 - };
235 + src-tauri/src/quasi/
236 + ├── mod.rs # The router chain and the document's stylesheet list
237 + ├── shell.rs # Not a screen: the navigation, and the running-timer band
238 + ├── assets.rs # Not a screen: stylesheets, scripts and fonts
239 + ├── theming.rs # The chosen theme, served as a stylesheet
240 + ├── tasks.rs, task_list.rs, board.rs
241 + ├── projects.rs, events.rs, contacts.rs, emails.rs, compose.rs
242 + ├── day_planning.rs, time_tracking.rs
243 + ├── weekly_review.rs, monthly_review.rs
244 + ├── settings.rs, data.rs, problems.rs
224 245 ```
225 246
226 - ### Module Pattern
247 + A screen module reads `AppState` repositories directly, or calls a command as a
248 + plain function, and returns a description. Each module also carries its own
249 + findings about what a real screen needed that the description layer could not
250 + say, because a finding that lives only in a commit message is a finding nobody
251 + acts on.
227 252
228 - Each domain module is wrapped in an IIFE and exposes its public API through the namespace:
253 + ### The five surviving scripts
229 254
230 - ```javascript
231 - (function() {
232 - 'use strict';
233 - // Private state and helpers
234 - async function load() { ... }
235 - function openNew() { ... }
255 + Each does something a description cannot say, and there is no sixth without that
256 + being true of it.
236 257
237 - // Public API
238 - GoingsOn.myModule = { load, openNew };
239 - })();
240 - ```
258 + | Script | Job |
259 + |---|---|
260 + | `host.js` | the few host jobs that need IPC, such as opening the compose window |
261 + | `quasi-clock.js` | a ticking clock, which is time passing rather than state changing |
262 + | `quasi-download.js` | handing the viewer a file |
263 + | `quasi-fill.js` | refilling a form the server rejected |
264 + | `quasi-selection.js` | multi-select across a list |
241 265
242 - ### Pre-computed Response Fields
266 + htmx is vendored under `frontend/vendor/`.
243 267
244 - Rust response types include pre-computed display values so JS never calculates dates, formatting, or derived state:
268 + ### Pre-computed response fields
269 +
270 + Response types still carry pre-computed display values, so nothing downstream
271 + calculates dates, formatting or derived state. The consumer changed from a JS
272 + render function to a description, and the reason did not.
245 273
246 274 | Response Type | Pre-computed Fields |
247 275 |--------------|---------------------|
@@ -250,87 +278,36 @@
250 278 | EmailResponse | `receivedFormatted` |
251 279 | EmailAccountResponse | `lastSyncFormatted` |
252 280
253 - ### Centralized State
281 + ### What the swap left behind
254 282
255 - All shared data lives in `GoingsOn.state` with reactive pub/sub:
283 + Six features went out with `index.html` and come back as each is described. Max
284 + ruled (b) on goingson `da48cb6d`: no build ships until the flip is finished, so
285 + the interval where they are missing is an interval nobody is in. Compose,
286 + Settings > Sync and Settings > About are back. Settings > Sharing, Create Backup,
287 + the search box and the blocking graph are not, and the last two are refused
288 + rather than pending: they are things a host draws, not things a description says.
256 289
257 - ```javascript
258 - GoingsOn.state.set('tasks', updatedTasks); // Triggers subscribers
259 - GoingsOn.state.subscribe('tasks', (newVal, oldVal) => { ... });
260 - ```
290 + `check_vocabulary_use` is a real hole rather than an obsolete check. It asked
291 + which generated classes no markup emits, and the markup is `quasi-webview`'s
292 + emitter now, in another crate. goingson `43a682b0`.
261 293
262 - ### File Organization
263 -
264 - ```
265 - src-tauri/frontend/
266 - ├── css/
267 - │ └── styles.css # Design system + all components
268 - ├── fonts/
269 - │ └── Reglo-Bold.woff2 # Display font
270 - ├── js/
271 - │ ├── goingson.js # Namespace root (window.GoingsOn)
272 - │ ├── api.js # Tauri IPC abstraction
273 - │ ├── state.js # Centralized state + pub/sub
274 - │ ├── utils.js # Escaping, validation, debounce
275 - │ ├── router.js # View routing
276 - │ ├── app.js # App initialization, menu listeners
277 - │ │
278 - │ ├── components.js # Toast, confirm dialog
279 - │ ├── components-modal.js # Modal system
280 - │ ├── form-modal.js # Form modal (openFormModal)
281 - │ ├── navigation.js # View switching, sidebar
282 - │ ├── keyboard.js # Keyboard shortcuts
283 - │ ├── selection-manager.js # Multi-select with shift/ctrl
284 - │ ├── pagination-manager.js # Page navigation
285 - │ ├── virtual-scroller.js # Virtual scrolling for large lists
286 - │ ├── card-grid-scroller.js # Virtual scrolling for multi-column card grids
287 - │ ├── context-menus.js # Right-click context menus
288 - │ ├── bulk-actions.js # Multi-select bulk operations
289 - │ ├── touch.js # Touch event handling
290 - │ ├── mobile.js # Mobile-specific behavior
291 - │ │
292 - │ ├── tasks.js # Task list, CRUD
293 - │ ├── tasks-render.js # Task rendering
294 - │ ├── tasks-kanban.js # Kanban board view
295 - │ ├── projects.js # Project list, detail, CRUD
296 - │ ├── projects-render.js # Project rendering
297 - │ ├── events.js # Event list, CRUD
298 - │ ├── emails.js # Email list, threading, CRUD
299 - │ ├── email-accounts.js # Email account management
300 - │ ├── contacts.js # Contact CRUD
301 - │ ├── contacts-render.js # Contact rendering
302 - │ │
303 - │ ├── day-planning.js # Time-blocking day planner
304 - │ ├── day-planning-render.js # Day plan rendering
305 - │ ├── weekly-review.js # Weekly review workflow
306 - │ ├── weekly-review-render.js # Weekly review rendering
307 - │ ├── snooze.js # Snooze modal + actions
308 - │ ├── settings.js # Settings, export
309 - │ ├── settings-sync.js # Cloud sync settings
310 - │ ├── themes.js # Theme switching
311 - │ ├── export.js # Data export
312 - │ ├── import.js # Data import from JSON
313 - │ ├── seed-data.js # Demo data seeding
314 - │ │
315 - │ └── tests/
316 - │ ├── test-runner.js # Test framework
317 - │ └── run.js # Test execution
318 - └── index.html # Entry point
319 - ```
294 + For the stylesheets and what may be written by hand, see `design-system.md`.
320 295
321 296 ## Data Flow
322 297
323 298 ```
324 - Frontend (JS)
325 - → invoke("command_name", { args })
326 - → Tauri IPC
327 - → commands/module.rs
328 - → Repository trait method
299 + A request arrives at quasi://localhost/<path>
300 + → the screen module that claimed that route
301 + → AppState repository, or a command called as a plain function
329 302 → SQLite query
330 - → Response (with pre-computed display fields) → Frontend
331 - → JS renders pre-computed values directly to DOM
303 + → Response (with pre-computed display fields)
304 + → the module returns a description
305 + → quasi-webview renders it to HTML, escaping as it goes
332 306 ```
333 307
308 + htmx swaps the body on a navigation. The head is not swapped, which is why a
309 + pinned theme change takes effect at the next launch rather than immediately.
310 +
334 311 ## Key Design Decisions
335 312
336 313 ### Clean Architecture
@@ -338,13 +315,12 @@
338 315 - Repository traits define contracts, implementations are separate crates
339 316 - Easy to swap databases or add new ones
340 317
341 - ### Vanilla Frontend
342 - - No JavaScript framework: vanilla JS with IIFE modules
343 - - All code under `GoingsOn` global namespace (no `window.*` exports)
344 - - Centralized state via `GoingsOn.state` with pub/sub reactivity
345 - - IPC via Tauri invoke
346 - - Virtual scrolling for large lists (`GoingsOn.VirtualScroller`)
347 - - Optimized for desktop-class performance
318 + ### Described screens
319 + - A screen states what it is, not how it is drawn. The renderer decides the rest
320 + - Escaping is typed at the renderer, so no call site can forget it
321 + - One description serves any host quasi has a renderer for
322 + - A need the vocabulary cannot state is a gap filed against quasi, never a
323 + local workaround
348 324
349 325 ### TaskWarrior-Inspired Features
350 326 - Urgency calculation algorithm
@@ -353,8 +329,10 @@
353 329
354 330 ## Testing Strategy
355 331
356 - - 658 Rust tests + 48 JS tests
357 - - Unit tests in core crate for business logic
358 - - Integration tests for repository implementations
359 - - Tauri command tests
360 - - JS tests in `frontend/js/tests/`
332 + - Unit tests in the core crate for business logic
333 + - Integration tests for the repository implementations
334 + - Command tests under `src-tauri/src/commands/tests/`
335 + - Screen tests beside the modules they cover
336 + - `build.rs` checks the stylesheet against what the renderer can emit, and the
337 + manifest against its storage-version ledger
338 + - `scripts/lint-frontend.sh` for the three stylesheet rules