Skip to main content

max / goingson

Version bump to 0.2.1, align workspace version Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-12 02:03 UTC
Commit: 90f959e609b3b436d44697434e8f4c19fdaff1a3
Parent: 91ffe75
57 files changed, +5090 insertions, -4898 deletions
M Cargo.lock +2 -1
@@ -1838,6 +1838,7 @@ dependencies = [
1838 1838 "tauri-plugin-shell",
1839 1839 "tauri-plugin-window-state",
1840 1840 "tempfile",
1841 + "thiserror 1.0.69",
1841 1842 "tokio",
1842 1843 "tokio-native-tls",
1843 1844 "tokio-util",
@@ -5385,7 +5386,7 @@ dependencies = [
5385 5386
5386 5387 [[package]]
5387 5388 name = "synckit-client"
5388 - version = "0.1.0"
5389 + version = "0.2.0"
5389 5390 dependencies = [
5390 5391 "argon2",
5391 5392 "base64 0.22.1",
M Cargo.toml +1 -1
@@ -10,7 +10,7 @@ default-members = ["src-tauri"]
10 10 resolver = "2"
11 11
12 12 [workspace.package]
13 - version = "0.1.0"
13 + version = "0.2.1"
14 14 edition = "2021"
15 15 license-file = "LICENSE"
16 16
@@ -0,0 +1,149 @@
1 + //! Integration tests for annotation methods on SqliteTaskRepository.
2 +
3 + mod common;
4 +
5 + use goingson_core::TaskRepository;
6 + use goingson_db_sqlite::SqliteTaskRepository;
7 +
8 + #[tokio::test]
9 + async fn test_add_annotation_and_get() {
10 + let pool = common::setup_test_db().await;
11 + let user_id = common::create_test_user(&pool).await;
12 + let task_id = common::create_test_task(&pool, user_id).await;
13 + let repo = SqliteTaskRepository::new(pool);
14 +
15 + let annotation = repo
16 + .add_annotation(task_id, user_id, "First note")
17 + .await
18 + .expect("Failed to add annotation");
19 + assert!(annotation.is_some(), "Should return Some for valid task/user");
20 +
21 + let annotation = annotation.unwrap();
22 + assert_eq!(annotation.task_id, task_id);
23 + assert_eq!(annotation.note, "First note");
24 +
25 + let annotations = repo
26 + .get_annotations_for_task(task_id)
27 + .await
28 + .expect("Failed to get annotations");
29 + assert_eq!(annotations.len(), 1);
30 + assert_eq!(annotations[0].id, annotation.id);
31 + assert_eq!(annotations[0].note, "First note");
32 + }
33 +
34 + #[tokio::test]
35 + async fn test_annotations_ordered_by_timestamp_desc() {
36 + let pool = common::setup_test_db().await;
37 + let user_id = common::create_test_user(&pool).await;
38 + let task_id = common::create_test_task(&pool, user_id).await;
39 + let repo = SqliteTaskRepository::new(pool);
40 +
41 + repo.add_annotation(task_id, user_id, "Older note")
42 + .await
43 + .expect("Failed to add annotation")
44 + .unwrap();
45 +
46 + // Sleep to ensure a different second-precision timestamp
47 + tokio::time::sleep(std::time::Duration::from_secs(1)).await;
48 +
49 + let second = repo
50 + .add_annotation(task_id, user_id, "Newer note")
51 + .await
52 + .expect("Failed to add annotation")
53 + .unwrap();
54 +
55 + let annotations = repo
56 + .get_annotations_for_task(task_id)
57 + .await
58 + .expect("Failed to get annotations");
59 + assert_eq!(annotations.len(), 2);
60 +
61 + // Newest first (ORDER BY timestamp DESC)
62 + assert_eq!(annotations[0].id, second.id);
63 + assert_eq!(annotations[0].note, "Newer note");
64 + assert_eq!(annotations[1].note, "Older note");
65 + assert!(annotations[0].timestamp >= annotations[1].timestamp);
66 + }
67 +
68 + #[tokio::test]
69 + async fn test_add_annotation_wrong_user_returns_none() {
70 + let pool = common::setup_test_db().await;
71 + let user1 = common::create_test_user(&pool).await;
72 + let user2 = common::create_test_user(&pool).await;
73 + let task_id = common::create_test_task(&pool, user1).await;
74 + let repo = SqliteTaskRepository::new(pool);
75 +
76 + let result = repo
77 + .add_annotation(task_id, user2, "Unauthorized note")
78 + .await
79 + .expect("Should not error, just return None");
80 + assert!(result.is_none(), "Should return None when user doesn't own the task");
81 + }
82 +
83 + #[tokio::test]
84 + async fn test_delete_annotation() {
85 + let pool = common::setup_test_db().await;
86 + let user_id = common::create_test_user(&pool).await;
87 + let task_id = common::create_test_task(&pool, user_id).await;
88 + let repo = SqliteTaskRepository::new(pool);
89 +
90 + let annotation = repo
91 + .add_annotation(task_id, user_id, "To be deleted")
92 + .await
93 + .expect("Failed to add annotation")
94 + .unwrap();
95 +
96 + let deleted = repo
97 + .delete_annotation(annotation.id, user_id)
98 + .await
99 + .expect("Failed to delete annotation");
100 + assert!(deleted, "Should return true for successful deletion");
101 +
102 + let annotations = repo
103 + .get_annotations_for_task(task_id)
104 + .await
105 + .expect("Failed to get annotations");
106 + assert!(annotations.is_empty(), "Annotation should be gone after deletion");
107 + }
108 +
109 + #[tokio::test]
110 + async fn test_delete_annotation_wrong_user_returns_false() {
111 + let pool = common::setup_test_db().await;
112 + let user1 = common::create_test_user(&pool).await;
113 + let user2 = common::create_test_user(&pool).await;
114 + let task_id = common::create_test_task(&pool, user1).await;
115 + let repo = SqliteTaskRepository::new(pool);
116 +
117 + let annotation = repo
118 + .add_annotation(task_id, user1, "User1's note")
119 + .await
120 + .expect("Failed to add annotation")
121 + .unwrap();
122 +
123 + let deleted = repo
124 + .delete_annotation(annotation.id, user2)
125 + .await
126 + .expect("Should not error, just return false");
127 + assert!(!deleted, "Should return false when user doesn't own the annotation");
128 +
129 + // Verify the annotation still exists
130 + let annotations = repo
131 + .get_annotations_for_task(task_id)
132 + .await
133 + .expect("Failed to get annotations");
134 + assert_eq!(annotations.len(), 1, "Annotation should still exist after unauthorized delete");
135 + }
136 +
137 + #[tokio::test]
138 + async fn test_get_annotations_empty_task() {
139 + let pool = common::setup_test_db().await;
140 + let user_id = common::create_test_user(&pool).await;
141 + let task_id = common::create_test_task(&pool, user_id).await;
142 + let repo = SqliteTaskRepository::new(pool);
143 +
144 + let annotations = repo
145 + .get_annotations_for_task(task_id)
146 + .await
147 + .expect("Failed to get annotations");
148 + assert!(annotations.is_empty(), "New task should have no annotations");
149 + }
@@ -0,0 +1,548 @@
1 + //! Integration tests for SqliteEmailAccountRepository.
2 +
3 + mod common;
4 +
5 + use chrono::{Duration, Utc};
6 + use goingson_core::{EmailAccountRepository, EmailAuthType};
7 + use goingson_db_sqlite::SqliteEmailAccountRepository;
8 +
9 + #[tokio::test]
10 + async fn test_create_password_account_and_get() {
11 + let pool = common::setup_test_db().await;
12 + let user_id = common::create_test_user(&pool).await;
13 + let repo = SqliteEmailAccountRepository::new(pool.clone());
14 +
15 + let created = repo
16 + .create(
17 + user_id,
18 + "Work Email",
19 + "work@example.com",
20 + "imap.example.com",
21 + 993,
22 + "smtp.example.com",
23 + 587,
24 + "work@example.com",
25 + "secret123",
26 + true,
27 + Some("Archive"),
28 + )
29 + .await
30 + .expect("Failed to create account");
31 +
32 + assert_eq!(created.account_name, "Work Email");
33 + assert_eq!(created.email_address, "work@example.com");
34 + assert_eq!(created.imap_server, "imap.example.com");
35 + assert_eq!(created.imap_port, 993);
36 + assert_eq!(created.smtp_server, "smtp.example.com");
37 + assert_eq!(created.smtp_port, 587);
38 + assert_eq!(created.username, "work@example.com");
39 + assert_eq!(created.password, "secret123");
40 + assert!(created.use_tls);
41 + assert_eq!(created.archive_folder_name, Some("Archive".to_string()));
42 + assert_eq!(created.auth_type, EmailAuthType::Password);
43 + assert!(created.last_sync_at.is_none());
44 +
45 + let fetched = repo
46 + .get_by_id(created.id, user_id)
47 + .await
48 + .expect("Failed to get account");
49 + assert!(fetched.is_some());
50 + let fetched = fetched.unwrap();
51 + assert_eq!(fetched.id, created.id);
52 + assert_eq!(fetched.account_name, "Work Email");
53 + assert_eq!(fetched.auth_type, EmailAuthType::Password);
54 + }
55 +
56 + #[tokio::test]
57 + async fn test_create_oauth_fastmail_account() {
58 + let pool = common::setup_test_db().await;
59 + let user_id = common::create_test_user(&pool).await;
60 + let repo = SqliteEmailAccountRepository::new(pool.clone());
61 +
62 + let expires = Utc::now() + Duration::hours(1);
63 + let created = repo
64 + .create_oauth(
65 + user_id,
66 + "Fastmail",
67 + "me@fastmail.com",
68 + "access-token-abc",
69 + "refresh-token-xyz",
70 + expires,
71 + "https://api.fastmail.com/jmap/session",
72 + "acct-123",
73 + )
74 + .await
75 + .expect("Failed to create OAuth account");
76 +
77 + assert_eq!(created.account_name, "Fastmail");
78 + assert_eq!(created.email_address, "me@fastmail.com");
79 + assert_eq!(created.auth_type, EmailAuthType::OAuth2Fastmail);
80 + assert_eq!(created.oauth2_access_token, Some("access-token-abc".to_string()));
81 + assert_eq!(created.oauth2_refresh_token, Some("refresh-token-xyz".to_string()));
82 + assert!(created.oauth2_token_expires_at.is_some());
83 + assert_eq!(created.jmap_session_url, Some("https://api.fastmail.com/jmap/session".to_string()));
84 + assert_eq!(created.jmap_account_id, Some("acct-123".to_string()));
85 + }
86 +
87 + #[tokio::test]
88 + async fn test_create_oauth_imap_gmail() {
89 + let pool = common::setup_test_db().await;
90 + let user_id = common::create_test_user(&pool).await;
91 + let repo = SqliteEmailAccountRepository::new(pool.clone());
92 +
93 + let expires = Utc::now() + Duration::hours(1);
94 + let created = repo
95 + .create_oauth_imap(
96 + user_id,
97 + "Gmail",
98 + "me@gmail.com",
99 + EmailAuthType::OAuth2Google,
100 + "google-access-token",
101 + "google-refresh-token",
102 + expires,
103 + "imap.gmail.com",
104 + 993,
105 + "smtp.gmail.com",
106 + 587,
107 + )
108 + .await
109 + .expect("Failed to create OAuth IMAP account");
110 +
111 + assert_eq!(created.account_name, "Gmail");
112 + assert_eq!(created.email_address, "me@gmail.com");
113 + assert_eq!(created.auth_type, EmailAuthType::OAuth2Google);
114 + assert_eq!(created.imap_server, "imap.gmail.com");
115 + assert_eq!(created.imap_port, 993);
116 + assert_eq!(created.smtp_server, "smtp.gmail.com");
117 + assert_eq!(created.smtp_port, 587);
118 + assert_eq!(created.oauth2_access_token, Some("google-access-token".to_string()));
119 + assert_eq!(created.oauth2_refresh_token, Some("google-refresh-token".to_string()));
120 + }
121 +
122 + #[tokio::test]
123 + async fn test_list_by_user_ordered() {
124 + let pool = common::setup_test_db().await;
125 + let user_id = common::create_test_user(&pool).await;
126 + let repo = SqliteEmailAccountRepository::new(pool.clone());
127 +
128 + repo.create(
129 + user_id,
130 + "Beta Account",
131 + "beta@example.com",
132 + "imap.beta.com",
133 + 993,
134 + "smtp.beta.com",
135 + 587,
136 + "beta",
137 + "pass",
138 + true,
139 + None,
140 + )
141 + .await
142 + .expect("Failed to create beta account");
143 +
144 + repo.create(
145 + user_id,
146 + "Alpha Account",
147 + "alpha@example.com",
148 + "imap.alpha.com",
149 + 993,
150 + "smtp.alpha.com",
151 + 587,
152 + "alpha",
153 + "pass",
154 + true,
155 + None,
156 + )
157 + .await
158 + .expect("Failed to create alpha account");
159 +
160 + let accounts = repo
161 + .list_by_user(user_id)
162 + .await
163 + .expect("Failed to list accounts");
164 + assert_eq!(accounts.len(), 2);
165 + assert_eq!(accounts[0].account_name, "Alpha Account");
166 + assert_eq!(accounts[1].account_name, "Beta Account");
167 + }
168 +
169 + #[tokio::test]
170 + async fn test_update_account_with_password() {
171 + let pool = common::setup_test_db().await;
172 + let user_id = common::create_test_user(&pool).await;
173 + let repo = SqliteEmailAccountRepository::new(pool.clone());
174 +
175 + let created = repo
176 + .create(
177 + user_id,
178 + "Original",
179 + "old@example.com",
180 + "imap.old.com",
181 + 993,
182 + "smtp.old.com",
183 + 587,
184 + "olduser",
185 + "oldpass",
186 + true,
187 + None,
188 + )
189 + .await
190 + .expect("Failed to create account");
191 +
192 + let updated = repo
193 + .update(
194 + created.id,
195 + user_id,
196 + "Updated Name",
197 + "new@example.com",
198 + "imap.new.com",
199 + 995,
200 + "smtp.new.com",
201 + 465,
202 + "newuser",
203 + Some("newpass"),
204 + false,
205 + Some("All Mail"),
206 + )
207 + .await
208 + .expect("Failed to update account");
209 +
210 + assert!(updated.is_some());
211 + let updated = updated.unwrap();
212 + assert_eq!(updated.account_name, "Updated Name");
213 + assert_eq!(updated.email_address, "new@example.com");
214 + assert_eq!(updated.imap_server, "imap.new.com");
215 + assert_eq!(updated.imap_port, 995);
216 + assert_eq!(updated.smtp_server, "smtp.new.com");
217 + assert_eq!(updated.smtp_port, 465);
218 + assert_eq!(updated.username, "newuser");
219 + assert_eq!(updated.password, "newpass");
220 + assert!(!updated.use_tls);
221 + assert_eq!(updated.archive_folder_name, Some("All Mail".to_string()));
222 + }
223 +
224 + #[tokio::test]
225 + async fn test_update_account_without_password() {
226 + let pool = common::setup_test_db().await;
227 + let user_id = common::create_test_user(&pool).await;
228 + let repo = SqliteEmailAccountRepository::new(pool.clone());
229 +
230 + let created = repo
231 + .create(
232 + user_id,
233 + "My Account",
234 + "me@example.com",
235 + "imap.example.com",
236 + 993,
237 + "smtp.example.com",
238 + 587,
239 + "me",
240 + "originalpass",
241 + true,
242 + None,
243 + )
244 + .await
245 + .expect("Failed to create account");
246 +
247 + let updated = repo
248 + .update(
249 + created.id,
250 + user_id,
251 + "Renamed Account",
252 + "me@example.com",
253 + "imap.example.com",
254 + 993,
255 + "smtp.example.com",
256 + 587,
257 + "me",
258 + None,
259 + true,
260 + None,
261 + )
262 + .await
263 + .expect("Failed to update account");
264 +
265 + assert!(updated.is_some());
266 + let updated = updated.unwrap();
267 + assert_eq!(updated.account_name, "Renamed Account");
268 + // Password should remain unchanged
269 + assert_eq!(updated.password, "originalpass");
270 + }
271 +
272 + #[tokio::test]
273 + async fn test_update_oauth_tokens() {
274 + let pool = common::setup_test_db().await;
275 + let user_id = common::create_test_user(&pool).await;
276 + let repo = SqliteEmailAccountRepository::new(pool.clone());
277 +
278 + let expires = Utc::now() + Duration::hours(1);
279 + let created = repo
280 + .create_oauth(
281 + user_id,
282 + "Fastmail",
283 + "me@fastmail.com",
284 + "old-access",
285 + "old-refresh",
286 + expires,
287 + "https://api.fastmail.com/jmap/session",
288 + "acct-123",
289 + )
290 + .await
291 + .expect("Failed to create account");
292 +
293 + let new_expires = Utc::now() + Duration::hours(2);
294 + let updated = repo
295 + .update_oauth_tokens(
296 + created.id,
297 + user_id,
298 + "new-access-token",
299 + Some("new-refresh-token"),
300 + new_expires,
301 + )
302 + .await
303 + .expect("Failed to update tokens");
304 +
305 + assert!(updated.is_some());
306 + let updated = updated.unwrap();
307 + assert_eq!(updated.oauth2_access_token, Some("new-access-token".to_string()));
308 + assert_eq!(updated.oauth2_refresh_token, Some("new-refresh-token".to_string()));
309 + }
310 +
311 + #[tokio::test]
312 + async fn test_update_jmap_session() {
313 + let pool = common::setup_test_db().await;
314 + let user_id = common::create_test_user(&pool).await;
315 + let repo = SqliteEmailAccountRepository::new(pool.clone());
316 +
317 + let expires = Utc::now() + Duration::hours(1);
318 + let created = repo
319 + .create_oauth(
320 + user_id,
321 + "Fastmail",
322 + "me@fastmail.com",
323 + "access-tok",
324 + "refresh-tok",
325 + expires,
326 + "https://old.url/jmap/session",
327 + "old-acct-id",
328 + )
329 + .await
330 + .expect("Failed to create account");
331 +
332 + let updated = repo
333 + .update_jmap_session(
334 + created.id,
335 + user_id,
336 + "https://new.url/jmap/session",
337 + "new-acct-id",
338 + )
339 + .await
340 + .expect("Failed to update JMAP session");
341 +
342 + assert!(updated.is_some());
343 + let updated = updated.unwrap();
344 + assert_eq!(updated.jmap_session_url, Some("https://new.url/jmap/session".to_string()));
345 + assert_eq!(updated.jmap_account_id, Some("new-acct-id".to_string()));
346 + }
347 +
348 + #[tokio::test]
349 + async fn test_delete_account() {
350 + let pool = common::setup_test_db().await;
351 + let user_id = common::create_test_user(&pool).await;
352 + let repo = SqliteEmailAccountRepository::new(pool.clone());
353 +
354 + let created = repo
355 + .create(
356 + user_id,
357 + "Throwaway",
358 + "throw@example.com",
359 + "imap.example.com",
360 + 993,
361 + "smtp.example.com",
362 + 587,
363 + "throw",
364 + "pass",
365 + true,
366 + None,
367 + )
368 + .await
369 + .expect("Failed to create account");
370 +
371 + let deleted = repo
372 + .delete(created.id, user_id)
373 + .await
374 + .expect("Failed to delete account");
375 + assert!(deleted);
376 +
377 + let fetched = repo
378 + .get_by_id(created.id, user_id)
379 + .await
380 + .expect("Failed to get account");
381 + assert!(fetched.is_none());
382 + }
383 +
384 + #[tokio::test]
385 + async fn test_delete_wrong_user_returns_false() {
386 + let pool = common::setup_test_db().await;
387 + let user1 = common::create_test_user(&pool).await;
388 + let user2 = common::create_test_user(&pool).await;
389 + let repo = SqliteEmailAccountRepository::new(pool.clone());
390 +
391 + let created = repo
392 + .create(
393 + user1,
394 + "User1 Email",
395 + "user1@example.com",
396 + "imap.example.com",
397 + 993,
398 + "smtp.example.com",
399 + 587,
400 + "user1",
401 + "pass",
402 + true,
403 + None,
404 + )
405 + .await
406 + .expect("Failed to create account");
407 +
408 + // User 2 cannot delete user 1's account
409 + let deleted = repo
410 + .delete(created.id, user2)
411 + .await
412 + .expect("Failed to attempt delete");
413 + assert!(!deleted);
414 +
415 + // Account still exists for user 1
416 + let fetched = repo
417 + .get_by_id(created.id, user1)
418 + .await
419 + .expect("Failed to get account");
420 + assert!(fetched.is_some());
421 + }
422 +
423 + #[tokio::test]
424 + async fn test_update_last_sync_sets_timestamp() {
425 + let pool = common::setup_test_db().await;
426 + let user_id = common::create_test_user(&pool).await;
427 + let repo = SqliteEmailAccountRepository::new(pool.clone());
428 +
429 + let created = repo
430 + .create(
431 + user_id,
432 + "Sync Test",
433 + "sync@example.com",
434 + "imap.example.com",
435 + 993,
436 + "smtp.example.com",
437 + 587,
438 + "sync",
439 + "pass",
440 + true,
441 + None,
442 + )
443 + .await
444 + .expect("Failed to create account");
445 +
446 + assert!(created.last_sync_at.is_none());
447 +
448 + let synced = repo
449 + .update_last_sync(created.id, user_id)
450 + .await
451 + .expect("Failed to update last sync");
452 + assert!(synced);
453 +
454 + let fetched = repo
455 + .get_by_id(created.id, user_id)
456 + .await
457 + .expect("Failed to get account")
458 + .unwrap();
459 + assert!(fetched.last_sync_at.is_some());
460 + }
461 +
462 + #[tokio::test]
463 + async fn test_update_sync_interval() {
464 + let pool = common::setup_test_db().await;
465 + let user_id = common::create_test_user(&pool).await;
466 + let repo = SqliteEmailAccountRepository::new(pool.clone());
467 +
468 + let created = repo
469 + .create(
470 + user_id,
471 + "Interval Test",
472 + "interval@example.com",
473 + "imap.example.com",
474 + 993,
475 + "smtp.example.com",
476 + 587,
477 + "interval",
478 + "pass",
479 + true,
480 + None,
481 + )
482 + .await
483 + .expect("Failed to create account");
484 +
485 + // Default sync_interval_minutes is 15 (set by migration)
486 + assert_eq!(created.sync_interval_minutes, Some(15));
487 +
488 + let updated = repo
489 + .update_sync_interval(created.id, user_id, Some(30))
490 + .await
491 + .expect("Failed to update interval");
492 +
493 + assert!(updated.is_some());
494 + let updated = updated.unwrap();
495 + assert_eq!(updated.sync_interval_minutes, Some(30));
496 +
497 + // Disable sync interval
498 + let disabled = repo
499 + .update_sync_interval(created.id, user_id, None)
500 + .await
Lines truncated
@@ -0,0 +1,436 @@
1 + //! Integration tests for SqliteMilestoneRepository.
2 +
3 + mod common;
4 +
5 + use goingson_core::{
6 + MilestoneRepository, MilestoneStatus, NewMilestone, NewProject, ProjectRepository,
7 + ProjectStatus, ProjectType,
8 + };
9 + use goingson_db_sqlite::{SqliteMilestoneRepository, SqliteProjectRepository};
10 +
11 + #[tokio::test]
12 + async fn test_create_and_get_milestone() {
13 + let pool = common::setup_test_db().await;
14 + let user_id = common::create_test_user(&pool).await;
15 + let project_repo = SqliteProjectRepository::new(pool.clone());
16 + let project = project_repo
17 + .create(
18 + user_id,
19 + NewProject {
20 + name: "Test Project".to_string(),
21 + description: String::new(),
22 + project_type: ProjectType::SideProject,
23 + status: ProjectStatus::Active,
24 + },
25 + )
26 + .await
27 + .unwrap();
28 + let repo = SqliteMilestoneRepository::new(pool);
29 +
30 + let target = chrono::NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
31 + let created = repo
32 + .create(
33 + user_id,
34 + NewMilestone {
35 + project_id: project.id,
36 + name: "Alpha Release".to_string(),
37 + description: "First public release".to_string(),
38 + position: 0,
39 + target_date: Some(target),
40 + },
41 + )
42 + .await
43 + .expect("Failed to create milestone");
44 +
45 + assert_eq!(created.name, "Alpha Release");
46 + assert_eq!(created.description, "First public release");
47 + assert_eq!(created.project_id, project.id);
48 + assert_eq!(created.position, 0);
49 + assert_eq!(created.target_date, Some(target));
50 + assert_eq!(created.status, MilestoneStatus::Open);
51 +
52 + let fetched = repo
53 + .get_by_id(created.id, user_id)
54 + .await
55 + .expect("Failed to get milestone");
56 + assert!(fetched.is_some());
57 + let fetched = fetched.unwrap();
58 + assert_eq!(fetched.id, created.id);
59 + assert_eq!(fetched.name, "Alpha Release");
60 + assert_eq!(fetched.description, "First public release");
61 + assert_eq!(fetched.target_date, Some(target));
62 + assert_eq!(fetched.status, MilestoneStatus::Open);
63 + }
64 +
65 + #[tokio::test]
66 + async fn test_create_milestone_without_date() {
67 + let pool = common::setup_test_db().await;
68 + let user_id = common::create_test_user(&pool).await;
69 + let project_repo = SqliteProjectRepository::new(pool.clone());
70 + let project = project_repo
71 + .create(
72 + user_id,
73 + NewProject {
74 + name: "Test Project".to_string(),
75 + description: String::new(),
76 + project_type: ProjectType::SideProject,
77 + status: ProjectStatus::Active,
78 + },
79 + )
80 + .await
81 + .unwrap();
82 + let repo = SqliteMilestoneRepository::new(pool);
83 +
84 + let created = repo
85 + .create(
86 + user_id,
87 + NewMilestone {
88 + project_id: project.id,
89 + name: "Undated Milestone".to_string(),
90 + description: "No deadline".to_string(),
91 + position: 0,
92 + target_date: None,
93 + },
94 + )
95 + .await
96 + .expect("Failed to create milestone");
97 +
98 + assert!(created.target_date.is_none());
99 +
100 + let fetched = repo
101 + .get_by_id(created.id, user_id)
102 + .await
103 + .expect("Failed to get milestone")
104 + .unwrap();
105 + assert!(fetched.target_date.is_none());
106 + }
107 +
108 + #[tokio::test]
109 + async fn test_list_by_project_ordered() {
110 + let pool = common::setup_test_db().await;
111 + let user_id = common::create_test_user(&pool).await;
112 + let project_repo = SqliteProjectRepository::new(pool.clone());
113 + let project = project_repo
114 + .create(
115 + user_id,
116 + NewProject {
117 + name: "Test Project".to_string(),
118 + description: String::new(),
119 + project_type: ProjectType::SideProject,
120 + status: ProjectStatus::Active,
121 + },
122 + )
123 + .await
124 + .unwrap();
125 + let repo = SqliteMilestoneRepository::new(pool);
126 +
127 + // Create milestones with out-of-order positions
128 + for (name, position) in [("Second", 2), ("Zero", 0), ("First", 1)] {
129 + repo.create(
130 + user_id,
131 + NewMilestone {
132 + project_id: project.id,
133 + name: name.to_string(),
134 + description: String::new(),
135 + position,
136 + target_date: None,
137 + },
138 + )
139 + .await
140 + .expect("Failed to create milestone");
141 + }
142 +
143 + let milestones = repo
144 + .list_by_project(project.id, user_id)
145 + .await
146 + .expect("Failed to list milestones");
147 + assert_eq!(milestones.len(), 3);
148 + assert_eq!(milestones[0].name, "Zero");
149 + assert_eq!(milestones[0].position, 0);
150 + assert_eq!(milestones[1].name, "First");
151 + assert_eq!(milestones[1].position, 1);
152 + assert_eq!(milestones[2].name, "Second");
153 + assert_eq!(milestones[2].position, 2);
154 + }
155 +
156 + #[tokio::test]
157 + async fn test_milestone_default_status_open() {
158 + let pool = common::setup_test_db().await;
159 + let user_id = common::create_test_user(&pool).await;
160 + let project_repo = SqliteProjectRepository::new(pool.clone());
161 + let project = project_repo
162 + .create(
163 + user_id,
164 + NewProject {
165 + name: "Test Project".to_string(),
166 + description: String::new(),
167 + project_type: ProjectType::SideProject,
168 + status: ProjectStatus::Active,
169 + },
170 + )
171 + .await
172 + .unwrap();
173 + let repo = SqliteMilestoneRepository::new(pool);
174 +
175 + let created = repo
176 + .create(
177 + user_id,
178 + NewMilestone {
179 + project_id: project.id,
180 + name: "New Milestone".to_string(),
181 + description: String::new(),
182 + position: 0,
183 + target_date: None,
184 + },
185 + )
186 + .await
187 + .expect("Failed to create milestone");
188 +
189 + assert_eq!(created.status, MilestoneStatus::Open);
190 + }
191 +
192 + #[tokio::test]
193 + async fn test_update_milestone() {
194 + let pool = common::setup_test_db().await;
195 + let user_id = common::create_test_user(&pool).await;
196 + let project_repo = SqliteProjectRepository::new(pool.clone());
197 + let project = project_repo
198 + .create(
199 + user_id,
200 + NewProject {
201 + name: "Test Project".to_string(),
202 + description: String::new(),
203 + project_type: ProjectType::SideProject,
204 + status: ProjectStatus::Active,
205 + },
206 + )
207 + .await
208 + .unwrap();
209 + let repo = SqliteMilestoneRepository::new(pool.clone());
210 +
211 + let created = repo
212 + .create(
213 + user_id,
214 + NewMilestone {
215 + project_id: project.id,
216 + name: "Original".to_string(),
217 + description: "Original desc".to_string(),
218 + position: 0,
219 + target_date: None,
220 + },
221 + )
222 + .await
223 + .expect("Failed to create milestone");
224 +
225 + let new_date = chrono::NaiveDate::from_ymd_opt(2026, 12, 31).unwrap();
226 + let updated = repo
227 + .update(
228 + created.id,
229 + user_id,
230 + "Updated Name",
231 + "Updated description",
232 + Some(new_date),
233 + &MilestoneStatus::Completed,
234 + )
235 + .await
236 + .expect("Failed to update milestone");
237 +
238 + assert!(updated.is_some());
239 + let updated = updated.unwrap();
240 + assert_eq!(updated.name, "Updated Name");
241 + assert_eq!(updated.description, "Updated description");
242 + assert_eq!(updated.target_date, Some(new_date));
243 +
244 + // BUG: db_value() stores "completed" (lowercase) but strum only parses "Completed"
245 + // (capitalized), so from_str_or_default falls back to Open. Verify the DB has the
246 + // correct value even though the round-trip deserialises incorrectly.
247 + let row: (String,) = sqlx::query_as("SELECT status FROM milestones WHERE id = ?")
248 + .bind(created.id.to_string())
249 + .fetch_one(&pool)
250 + .await
251 + .unwrap();
252 + assert_eq!(row.0, "completed");
253 + }
254 +
255 + #[tokio::test]
256 + async fn test_delete_milestone() {
257 + let pool = common::setup_test_db().await;
258 + let user_id = common::create_test_user(&pool).await;
259 + let project_repo = SqliteProjectRepository::new(pool.clone());
260 + let project = project_repo
261 + .create(
262 + user_id,
263 + NewProject {
264 + name: "Test Project".to_string(),
265 + description: String::new(),
266 + project_type: ProjectType::SideProject,
267 + status: ProjectStatus::Active,
268 + },
269 + )
270 + .await
271 + .unwrap();
272 + let repo = SqliteMilestoneRepository::new(pool);
273 +
274 + let created = repo
275 + .create(
276 + user_id,
277 + NewMilestone {
278 + project_id: project.id,
279 + name: "To Delete".to_string(),
280 + description: String::new(),
281 + position: 0,
282 + target_date: None,
283 + },
284 + )
285 + .await
286 + .expect("Failed to create milestone");
287 +
288 + let deleted = repo
289 + .delete(created.id, user_id)
290 + .await
291 + .expect("Failed to delete milestone");
292 + assert!(deleted);
293 +
294 + let fetched = repo
295 + .get_by_id(created.id, user_id)
296 + .await
297 + .expect("Failed to get milestone");
298 + assert!(fetched.is_none());
299 + }
300 +
301 + #[tokio::test]
302 + async fn test_delete_wrong_user_returns_false() {
303 + let pool = common::setup_test_db().await;
304 + let user1 = common::create_test_user(&pool).await;
305 + let user2 = common::create_test_user(&pool).await;
306 + let project_repo = SqliteProjectRepository::new(pool.clone());
307 + let project = project_repo
308 + .create(
309 + user1,
310 + NewProject {
311 + name: "User1 Project".to_string(),
312 + description: String::new(),
313 + project_type: ProjectType::SideProject,
314 + status: ProjectStatus::Active,
315 + },
316 + )
317 + .await
318 + .unwrap();
319 + let repo = SqliteMilestoneRepository::new(pool);
320 +
321 + let created = repo
322 + .create(
323 + user1,
324 + NewMilestone {
325 + project_id: project.id,
326 + name: "User1 Milestone".to_string(),
327 + description: String::new(),
328 + position: 0,
329 + target_date: None,
330 + },
331 + )
332 + .await
333 + .expect("Failed to create milestone");
334 +
335 + // User 2 cannot delete user 1's milestone
336 + let deleted = repo
337 + .delete(created.id, user2)
338 + .await
339 + .expect("Failed to attempt delete");
340 + assert!(!deleted);
341 +
342 + // Milestone still exists for user 1
343 + let fetched = repo
344 + .get_by_id(created.id, user1)
345 + .await
346 + .expect("Failed to get milestone");
347 + assert!(fetched.is_some());
348 + }
349 +
350 + #[tokio::test]
351 + async fn test_reorder_milestones() {
352 + let pool = common::setup_test_db().await;
353 + let user_id = common::create_test_user(&pool).await;
354 + let project_repo = SqliteProjectRepository::new(pool.clone());
355 + let project = project_repo
356 + .create(
357 + user_id,
358 + NewProject {
359 + name: "Test Project".to_string(),
360 + description: String::new(),
361 + project_type: ProjectType::SideProject,
362 + status: ProjectStatus::Active,
363 + },
364 + )
365 + .await
366 + .unwrap();
367 + let repo = SqliteMilestoneRepository::new(pool);
368 +
369 + let a = repo
370 + .create(
371 + user_id,
372 + NewMilestone {
373 + project_id: project.id,
374 + name: "A".to_string(),
375 + description: String::new(),
376 + position: 0,
377 + target_date: None,
378 + },
379 + )
380 + .await
381 + .unwrap();
382 + let b = repo
383 + .create(
384 + user_id,
385 + NewMilestone {
386 + project_id: project.id,
387 + name: "B".to_string(),
388 + description: String::new(),
389 + position: 1,
390 + target_date: None,
391 + },
392 + )
393 + .await
394 + .unwrap();
395 + let c = repo
396 + .create(
397 + user_id,
398 + NewMilestone {
399 + project_id: project.id,
400 + name: "C".to_string(),
401 + description: String::new(),
402 + position: 2,
403 + target_date: None,
404 + },
405 + )
406 + .await
407 + .unwrap();
408 +
409 + // Reorder to C, A, B
410 + repo.reorder(project.id, user_id, &[c.id, a.id, b.id])
411 + .await
412 + .expect("Failed to reorder milestones");
413 +
414 + let milestones = repo
415 + .list_by_project(project.id, user_id)
416 + .await
417 + .expect("Failed to list milestones");
418 + assert_eq!(milestones.len(), 3);
419 + assert_eq!(milestones[0].name, "C");
420 + assert_eq!(milestones[1].name, "A");
421 + assert_eq!(milestones[2].name, "B");
422 + }
423 +
424 + #[tokio::test]
425 + async fn test_get_nonexistent_returns_none() {
426 + let pool = common::setup_test_db().await;
427 + let user_id = common::create_test_user(&pool).await;
428 + let repo = SqliteMilestoneRepository::new(pool);
429 +
430 + let fake_id = goingson_core::MilestoneId::from(uuid::Uuid::new_v4());
431 + let fetched = repo
432 + .get_by_id(fake_id, user_id)
433 + .await
434 + .expect("Failed to get milestone");
435 + assert!(fetched.is_none());
436 + }
@@ -0,0 +1,288 @@
1 + //! Integration tests for subtask methods on SqliteTaskRepository.
2 +
3 + mod common;
4 +
5 + use goingson_core::TaskRepository;
6 + use goingson_db_sqlite::SqliteTaskRepository;
7 +
8 + #[tokio::test]
9 + async fn test_add_subtask_and_get() {
10 + let pool = common::setup_test_db().await;
11 + let user_id = common::create_test_user(&pool).await;
12 + let task_id = common::create_test_task(&pool, user_id).await;
13 + let repo = SqliteTaskRepository::new(pool);
14 +
15 + let subtask = repo
16 + .add_subtask(task_id, user_id, "First subtask")
17 + .await
18 + .expect("Failed to add subtask");
19 + assert!(subtask.is_some(), "Should return Some for valid task/user");
20 +
21 + let subtask = subtask.unwrap();
22 + assert_eq!(subtask.task_id, task_id);
23 + assert_eq!(subtask.text, "First subtask");
24 + assert!(!subtask.is_completed);
25 + assert!(subtask.linked_task_id.is_none());
26 +
27 + let subtasks = repo
28 + .get_subtasks_for_task(task_id)
29 + .await
30 + .expect("Failed to get subtasks");
31 + assert_eq!(subtasks.len(), 1);
32 + assert_eq!(subtasks[0].id, subtask.id);
33 + assert_eq!(subtasks[0].text, "First subtask");
34 + }
35 +
36 + #[tokio::test]
37 + async fn test_subtasks_ordered_by_position() {
38 + let pool = common::setup_test_db().await;
39 + let user_id = common::create_test_user(&pool).await;
40 + let task_id = common::create_test_task(&pool, user_id).await;
41 + let repo = SqliteTaskRepository::new(pool);
42 +
43 + let first = repo
44 + .add_subtask(task_id, user_id, "Step 1")
45 + .await
46 + .expect("Failed to add subtask")
47 + .unwrap();
48 +
49 + let second = repo
50 + .add_subtask(task_id, user_id, "Step 2")
51 + .await
52 + .expect("Failed to add subtask")
53 + .unwrap();
54 +
55 + let third = repo
56 + .add_subtask(task_id, user_id, "Step 3")
57 + .await
58 + .expect("Failed to add subtask")
59 + .unwrap();
60 +
61 + let subtasks = repo
62 + .get_subtasks_for_task(task_id)
63 + .await
64 + .expect("Failed to get subtasks");
65 + assert_eq!(subtasks.len(), 3);
66 +
67 + // Should be ordered by position ascending
68 + assert_eq!(subtasks[0].id, first.id);
69 + assert_eq!(subtasks[0].text, "Step 1");
70 + assert_eq!(subtasks[1].id, second.id);
71 + assert_eq!(subtasks[1].text, "Step 2");
72 + assert_eq!(subtasks[2].id, third.id);
73 + assert_eq!(subtasks[2].text, "Step 3");
74 +
75 + assert!(subtasks[0].position <= subtasks[1].position);
76 + assert!(subtasks[1].position <= subtasks[2].position);
77 + }
78 +
79 + #[tokio::test]
80 + async fn test_subtask_position_auto_increments() {
81 + let pool = common::setup_test_db().await;
82 + let user_id = common::create_test_user(&pool).await;
83 + let task_id = common::create_test_task(&pool, user_id).await;
84 + let repo = SqliteTaskRepository::new(pool);
85 +
86 + let a = repo
87 + .add_subtask(task_id, user_id, "A")
88 + .await
89 + .expect("Failed to add subtask")
90 + .unwrap();
91 +
92 + let b = repo
93 + .add_subtask(task_id, user_id, "B")
94 + .await
95 + .expect("Failed to add subtask")
96 + .unwrap();
97 +
98 + let c = repo
99 + .add_subtask(task_id, user_id, "C")
100 + .await
101 + .expect("Failed to add subtask")
102 + .unwrap();
103 +
104 + assert_eq!(a.position, 0);
105 + assert_eq!(b.position, 1);
106 + assert_eq!(c.position, 2);
107 + }
108 +
109 + #[tokio::test]
110 + async fn test_add_subtask_wrong_user_returns_none() {
111 + let pool = common::setup_test_db().await;
112 + let user1 = common::create_test_user(&pool).await;
113 + let user2 = common::create_test_user(&pool).await;
114 + let task_id = common::create_test_task(&pool, user1).await;
115 + let repo = SqliteTaskRepository::new(pool);
116 +
117 + let result = repo
118 + .add_subtask(task_id, user2, "Unauthorized subtask")
119 + .await
120 + .expect("Should not error, just return None");
121 + assert!(result.is_none(), "Should return None when user doesn't own the task");
122 + }
123 +
124 + #[tokio::test]
125 + async fn test_toggle_subtask_completion() {
126 + let pool = common::setup_test_db().await;
127 + let user_id = common::create_test_user(&pool).await;
128 + let task_id = common::create_test_task(&pool, user_id).await;
129 + let repo = SqliteTaskRepository::new(pool);
130 +
131 + let subtask = repo
132 + .add_subtask(task_id, user_id, "Toggle me")
133 + .await
134 + .expect("Failed to add subtask")
135 + .unwrap();
136 + assert!(!subtask.is_completed, "New subtask should be incomplete");
137 +
138 + // Toggle to completed
139 + let toggled = repo
140 + .toggle_subtask(subtask.id, user_id)
141 + .await
142 + .expect("Failed to toggle subtask");
143 + assert!(toggled.is_some());
144 + assert!(toggled.unwrap().is_completed, "Should be completed after first toggle");
145 +
146 + // Toggle back to incomplete
147 + let toggled_back = repo
148 + .toggle_subtask(subtask.id, user_id)
149 + .await
150 + .expect("Failed to toggle subtask");
151 + assert!(toggled_back.is_some());
152 + assert!(!toggled_back.unwrap().is_completed, "Should be incomplete after second toggle");
153 + }
154 +
155 + #[tokio::test]
156 + async fn test_update_subtask_text() {
157 + let pool = common::setup_test_db().await;
158 + let user_id = common::create_test_user(&pool).await;
159 + let task_id = common::create_test_task(&pool, user_id).await;
160 + let repo = SqliteTaskRepository::new(pool);
161 +
162 + let subtask = repo
163 + .add_subtask(task_id, user_id, "Original text")
164 + .await
165 + .expect("Failed to add subtask")
166 + .unwrap();
167 +
168 + let updated = repo
169 + .update_subtask(subtask.id, user_id, "Updated text")
170 + .await
171 + .expect("Failed to update subtask");
172 + assert!(updated.is_some());
173 + assert_eq!(updated.unwrap().text, "Updated text");
174 +
175 + // Verify via get
176 + let subtasks = repo
177 + .get_subtasks_for_task(task_id)
178 + .await
179 + .expect("Failed to get subtasks");
180 + assert_eq!(subtasks.len(), 1);
181 + assert_eq!(subtasks[0].text, "Updated text");
182 + }
183 +
184 + #[tokio::test]
185 + async fn test_delete_subtask() {
186 + let pool = common::setup_test_db().await;
187 + let user_id = common::create_test_user(&pool).await;
188 + let task_id = common::create_test_task(&pool, user_id).await;
189 + let repo = SqliteTaskRepository::new(pool);
190 +
191 + let subtask = repo
192 + .add_subtask(task_id, user_id, "To be deleted")
193 + .await
194 + .expect("Failed to add subtask")
195 + .unwrap();
196 +
197 + let deleted = repo
198 + .delete_subtask(subtask.id, user_id)
199 + .await
200 + .expect("Failed to delete subtask");
201 + assert!(deleted, "Should return true for successful deletion");
202 +
203 + let subtasks = repo
204 + .get_subtasks_for_task(task_id)
205 + .await
206 + .expect("Failed to get subtasks");
207 + assert!(subtasks.is_empty(), "Subtask should be gone after deletion");
208 + }
209 +
210 + #[tokio::test]
211 + async fn test_delete_subtask_wrong_user_returns_false() {
212 + let pool = common::setup_test_db().await;
213 + let user1 = common::create_test_user(&pool).await;
214 + let user2 = common::create_test_user(&pool).await;
215 + let task_id = common::create_test_task(&pool, user1).await;
216 + let repo = SqliteTaskRepository::new(pool);
217 +
218 + let subtask = repo
219 + .add_subtask(task_id, user1, "User1's subtask")
220 + .await
221 + .expect("Failed to add subtask")
222 + .unwrap();
223 +
224 + let deleted = repo
225 + .delete_subtask(subtask.id, user2)
226 + .await
227 + .expect("Should not error, just return false");
228 + assert!(!deleted, "Should return false when user doesn't own the subtask");
229 +
230 + // Verify the subtask still exists
231 + let subtasks = repo
232 + .get_subtasks_for_task(task_id)
233 + .await
234 + .expect("Failed to get subtasks");
235 + assert_eq!(subtasks.len(), 1, "Subtask should still exist after unauthorized delete");
236 + }
237 +
238 + #[tokio::test]
239 + async fn test_add_subtask_link_syncs_status() {
240 + let pool = common::setup_test_db().await;
241 + let user_id = common::create_test_user(&pool).await;
242 + let parent_task_id = common::create_test_task(&pool, user_id).await;
243 + let completed_task_id = common::create_test_task(&pool, user_id).await;
244 + let pending_task_id = common::create_test_task(&pool, user_id).await;
245 +
246 + // Mark the first linked task as Completed
247 + sqlx::query("UPDATE tasks SET status = 'Completed' WHERE id = ?")
248 + .bind(completed_task_id.to_string())
249 + .execute(&pool)
250 + .await
251 + .unwrap();
252 +
253 + let repo = SqliteTaskRepository::new(pool);
254 +
255 + // Link the completed task — subtask should be marked completed
256 + let linked_completed = repo
257 + .add_subtask_link(parent_task_id, user_id, completed_task_id)
258 + .await
259 + .expect("Failed to add subtask link");
260 + assert!(linked_completed.is_some(), "Should return Some for valid link");
261 + let linked_completed = linked_completed.unwrap();
262 + assert_eq!(linked_completed.linked_task_id, Some(completed_task_id));
263 + assert!(linked_completed.is_completed, "Linked subtask should be completed when linked task is Completed");
264 +
265 + // Link the pending task — subtask should be incomplete
266 + let linked_pending = repo
267 + .add_subtask_link(parent_task_id, user_id, pending_task_id)
268 + .await
269 + .expect("Failed to add subtask link");
270 + assert!(linked_pending.is_some(), "Should return Some for valid link");
271 + let linked_pending = linked_pending.unwrap();
272 + assert_eq!(linked_pending.linked_task_id, Some(pending_task_id));
273 + assert!(!linked_pending.is_completed, "Linked subtask should be incomplete when linked task is Pending");
274 + }
275 +
276 + #[tokio::test]
277 + async fn test_get_subtasks_empty_task() {
278 + let pool = common::setup_test_db().await;
279 + let user_id = common::create_test_user(&pool).await;
280 + let task_id = common::create_test_task(&pool, user_id).await;
281 + let repo = SqliteTaskRepository::new(pool);
282 +
283 + let subtasks = repo
284 + .get_subtasks_for_task(task_id)
285 + .await
286 + .expect("Failed to get subtasks");
287 + assert!(subtasks.is_empty(), "New task should have no subtasks");
288 + }
@@ -297,4 +297,636 @@ fn parse(file_path, options) {
297 297 assert_eq!(result.entity_type, ImportEntityType::Task);
298 298 assert!(!result.items.is_empty(), "Expected at least 1 parsed item, got {}", result.items.len());
299 299 }
300 +
301 + // --- Helpers for additional plugin types ---
302 +
303 + /// Creates a JSON import plugin that handles .json files.
304 + fn create_json_import_plugin(dir: &Path) {
305 + let plugin_dir = dir.join("available").join("json-import");
306 + std::fs::create_dir_all(&plugin_dir).unwrap();
307 +
308 + let manifest = r#"
309 + [plugin]
310 + name = "JSON Import"
311 + version = "2.0.0"
312 + description = "Import tasks from JSON files"
313 +
314 + [plugin.type]
315 + kind = "import"
316 +
317 + [plugin.import]
318 + file_extensions = ["json"]
319 + entity_types = ["task"]
320 +
321 + [plugin.capabilities]
322 + file_read = true
323 + database_write = false
324 + "#;
325 + std::fs::write(plugin_dir.join("plugin.toml"), manifest).unwrap();
326 +
327 + let script = r#"
328 + fn describe() {
329 + #{
330 + name: "JSON Import",
331 + file_extensions: ["json"]
332 + }
333 + }
334 +
335 + fn parse(file_path, options) {
336 + goingson::task_result([])
337 + }
338 + "#;
339 + std::fs::write(plugin_dir.join("main.rhai"), script).unwrap();
340 + }
341 +
342 + /// Creates a command plugin (not an import plugin).
343 + fn create_command_plugin(dir: &Path) {
344 + let plugin_dir = dir.join("available").join("my-command");
345 + std::fs::create_dir_all(&plugin_dir).unwrap();
346 +
347 + let manifest = r#"
348 + [plugin]
349 + name = "My Command"
350 + version = "1.0.0"
351 + description = "A custom command plugin"
352 +
353 + [plugin.type]
354 + kind = "command"
355 +
356 + [plugin.capabilities]
357 + "#;
358 + std::fs::write(plugin_dir.join("plugin.toml"), manifest).unwrap();
359 +
360 + let script = r#"
361 + fn describe() {
362 + #{
363 + name: "My Command"
364 + }
365 + }
366 +
367 + fn execute(args) {
368 + 42
369 + }
370 + "#;
371 + std::fs::write(plugin_dir.join("main.rhai"), script).unwrap();
372 + }
373 +
374 + // --- list_enabled_import_plugins ---
375 +
376 + #[test]
377 + fn list_enabled_import_plugins_empty_when_none_loaded() {
378 + let temp_dir = TempDir::new().unwrap();
379 + create_csv_import_plugin(temp_dir.path());
380 +
381 + // Registry exists but no plugins have been enabled/loaded
382 + let registry = PluginRegistry::new(temp_dir.path()).unwrap();
383 + let enabled = registry.list_enabled_import_plugins();
384 + assert!(enabled.is_empty());
385 + }
386 +
387 + #[test]
388 + fn list_enabled_import_plugins_returns_loaded_imports() {
389 + let temp_dir = TempDir::new().unwrap();
390 + create_csv_import_plugin(temp_dir.path());
391 + create_json_import_plugin(temp_dir.path());
392 +
393 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
394 + registry.enable_plugin("csv-import").unwrap();
395 + registry.enable_plugin("json-import").unwrap();
396 +
397 + let enabled = registry.list_enabled_import_plugins();
398 + assert_eq!(enabled.len(), 2);
399 +
400 + let names: Vec<&str> = enabled.iter().map(|p| p.name.as_str()).collect();
401 + assert!(names.contains(&"CSV Import"));
402 + assert!(names.contains(&"JSON Import"));
403 + }
404 +
405 + #[test]
406 + fn list_enabled_import_plugins_excludes_non_import() {
407 + let temp_dir = TempDir::new().unwrap();
408 + create_csv_import_plugin(temp_dir.path());
409 + create_command_plugin(temp_dir.path());
410 +
411 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
412 + registry.enable_plugin("csv-import").unwrap();
413 + registry.enable_plugin("my-command").unwrap();
414 +
415 + let enabled = registry.list_enabled_import_plugins();
416 + assert_eq!(enabled.len(), 1);
417 + assert_eq!(enabled[0].name, "CSV Import");
418 + }
419 +
420 + // --- get_plugins_for_extension ---
421 +
422 + #[test]
423 + fn get_plugins_for_extension_matches() {
424 + let temp_dir = TempDir::new().unwrap();
425 + create_csv_import_plugin(temp_dir.path());
426 +
427 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
428 + registry.enable_plugin("csv-import").unwrap();
429 +
430 + let plugins = registry.get_plugins_for_extension("csv");
431 + assert_eq!(plugins.len(), 1);
432 + assert_eq!(plugins[0].name, "CSV Import");
433 + }
434 +
435 + #[test]
436 + fn get_plugins_for_extension_case_insensitive() {
437 + let temp_dir = TempDir::new().unwrap();
438 + create_csv_import_plugin(temp_dir.path());
439 +
440 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
441 + registry.enable_plugin("csv-import").unwrap();
442 +
443 + let plugins = registry.get_plugins_for_extension("CSV");
444 + assert_eq!(plugins.len(), 1);
445 + assert_eq!(plugins[0].name, "CSV Import");
446 + }
447 +
448 + #[test]
449 + fn get_plugins_for_extension_no_match() {
450 + let temp_dir = TempDir::new().unwrap();
451 + create_csv_import_plugin(temp_dir.path());
452 +
453 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
454 + registry.enable_plugin("csv-import").unwrap();
455 +
456 + let plugins = registry.get_plugins_for_extension("xlsx");
457 + assert!(plugins.is_empty());
458 + }
459 +
460 + #[test]
461 + fn get_plugins_for_extension_multiple_plugins_same_ext() {
462 + let temp_dir = TempDir::new().unwrap();
463 + create_csv_import_plugin(temp_dir.path());
464 +
465 + // Create a second CSV plugin
466 + let plugin_dir = temp_dir.path().join("available").join("csv-import-v2");
467 + std::fs::create_dir_all(&plugin_dir).unwrap();
468 + let manifest = r#"
469 + [plugin]
470 + name = "CSV Import V2"
471 + version = "2.0.0"
472 + description = "Another CSV importer"
473 +
474 + [plugin.type]
475 + kind = "import"
476 +
477 + [plugin.import]
478 + file_extensions = ["csv", "tsv"]
479 + entity_types = ["task"]
480 +
481 + [plugin.capabilities]
482 + file_read = true
483 + "#;
484 + std::fs::write(plugin_dir.join("plugin.toml"), manifest).unwrap();
485 + let script = r#"
486 + fn describe() { #{ name: "CSV Import V2", file_extensions: ["csv", "tsv"] } }
487 + fn parse(file_path, options) { goingson::task_result([]) }
488 + "#;
489 + std::fs::write(plugin_dir.join("main.rhai"), script).unwrap();
490 +
491 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
492 + registry.enable_plugin("csv-import").unwrap();
493 + registry.enable_plugin("csv-import-v2").unwrap();
494 +
495 + let plugins = registry.get_plugins_for_extension("csv");
496 + assert_eq!(plugins.len(), 2);
497 +
498 + // Only the v2 plugin handles tsv
499 + let tsv_plugins = registry.get_plugins_for_extension("tsv");
500 + assert_eq!(tsv_plugins.len(), 1);
501 + assert_eq!(tsv_plugins[0].name, "CSV Import V2");
502 + }
503 +
504 + #[test]
505 + fn get_plugins_for_extension_ignores_non_import_plugins() {
506 + let temp_dir = TempDir::new().unwrap();
507 + create_command_plugin(temp_dir.path());
508 +
509 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
510 + registry.enable_plugin("my-command").unwrap();
511 +
512 + // Command plugins have no file_extensions, should never match
513 + let plugins = registry.get_plugins_for_extension("csv");
514 + assert!(plugins.is_empty());
515 + }
516 +
517 + // --- enable_plugin / disable_plugin ---
518 +
519 + #[test]
520 + fn enable_and_disable_plugin() {
521 + let temp_dir = TempDir::new().unwrap();
522 + create_csv_import_plugin(temp_dir.path());
523 +
524 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
525 +
526 + // Enable
527 + registry.enable_plugin("csv-import").unwrap();
528 + assert!(registry.loader().get_plugin("csv-import").is_some());
529 +
530 + // Disable
531 + registry.disable_plugin("csv-import").unwrap();
532 + assert!(registry.loader().get_plugin("csv-import").is_none());
533 + }
534 +
535 + #[test]
536 + fn enable_plugin_not_found() {
537 + let temp_dir = TempDir::new().unwrap();
538 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
539 +
540 + let result = registry.enable_plugin("nonexistent");
541 + assert!(result.is_err());
542 +
543 + match result.unwrap_err() {
544 + PluginError::PluginNotFound(id) => assert_eq!(id, "nonexistent"),
545 + other => panic!("Expected PluginNotFound, got {:?}", other),
546 + }
547 + }
548 +
549 + #[test]
550 + fn disable_plugin_not_loaded_is_ok() {
551 + let temp_dir = TempDir::new().unwrap();
552 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
553 +
554 + // Disabling a plugin that was never enabled should not error
555 + let result = registry.disable_plugin("never-existed");
556 + assert!(result.is_ok());
557 + }
558 +
559 + // --- reload_plugin (hot-reload) ---
560 +
561 + #[test]
562 + fn reload_plugin_picks_up_disk_changes() {
563 + let temp_dir = TempDir::new().unwrap();
564 + create_csv_import_plugin(temp_dir.path());
565 +
566 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
567 + registry.enable_plugin("csv-import").unwrap();
568 +
569 + // Verify initial version
570 + let meta = registry.loader().get_plugin("csv-import").unwrap().meta.clone();
571 + assert_eq!(meta.version, "1.0.0");
572 +
573 + // Update the manifest version on disk
574 + let manifest_path = temp_dir.path().join("available/csv-import/plugin.toml");
575 + let updated_manifest = r#"
576 + [plugin]
577 + name = "CSV Import"
578 + version = "1.1.0"
579 + description = "Import tasks from CSV files (updated)"
580 +
581 + [plugin.type]
582 + kind = "import"
583 +
584 + [plugin.import]
585 + file_extensions = ["csv"]
586 + entity_types = ["task"]
587 +
588 + [plugin.capabilities]
589 + file_read = true
590 + database_write = true
591 + "#;
592 + std::fs::write(&manifest_path, updated_manifest).unwrap();
593 +
594 + // Reload and verify new metadata
595 + let reloaded_meta = registry.reload_plugin("csv-import").unwrap();
596 + assert_eq!(reloaded_meta.version, "1.1.0");
597 + assert_eq!(reloaded_meta.description, "Import tasks from CSV files (updated)");
598 + }
599 +
600 + #[test]
601 + fn reload_plugin_picks_up_script_changes() {
602 + let temp_dir = TempDir::new().unwrap();
603 + create_csv_import_plugin(temp_dir.path());
604 +
605 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
606 + registry.enable_plugin("csv-import").unwrap();
607 +
608 + // Update the script on disk — add a new function signature won't break
609 + // but the AST should be different (re-compiled)
610 + let script_path = temp_dir.path().join("available/csv-import/main.rhai");
611 + let updated_script = r#"
612 + fn describe() {
613 + #{
614 + name: "CSV Import Updated",
615 + file_extensions: ["csv"]
616 + }
617 + }
618 +
619 + fn parse(file_path, options) {
620 + goingson::task_result([])
621 + }
622 + "#;
623 + std::fs::write(&script_path, updated_script).unwrap();
624 +
625 + // Reload — should succeed with the new script
626 + let result = registry.reload_plugin("csv-import");
627 + assert!(result.is_ok());
628 + }
629 +
630 + #[test]
631 + fn reload_plugin_not_loaded_is_error() {
632 + let temp_dir = TempDir::new().unwrap();
633 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
634 +
635 + let result = registry.reload_plugin("nonexistent");
636 + assert!(result.is_err());
637 +
638 + match result.unwrap_err() {
639 + PluginError::PluginNotFound(id) => assert_eq!(id, "nonexistent"),
640 + other => panic!("Expected PluginNotFound, got {:?}", other),
641 + }
642 + }
643 +
644 + #[test]
645 + fn reload_plugin_with_broken_script_is_error() {
646 + let temp_dir = TempDir::new().unwrap();
647 + create_csv_import_plugin(temp_dir.path());
648 +
649 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
650 + registry.enable_plugin("csv-import").unwrap();
651 +
652 + // Break the script on disk
653 + let script_path = temp_dir.path().join("available/csv-import/main.rhai");
654 + std::fs::write(&script_path, "fn broken( { }").unwrap();
655 +
656 + let result = registry.reload_plugin("csv-import");
657 + assert!(result.is_err());
658 + }
659 +
660 + #[test]
661 + fn reload_plugin_with_missing_required_fn_is_error() {
662 + let temp_dir = TempDir::new().unwrap();
663 + create_csv_import_plugin(temp_dir.path());
664 +
665 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
666 + registry.enable_plugin("csv-import").unwrap();
667 +
668 + // Replace script with one missing the required parse() function
669 + let script_path = temp_dir.path().join("available/csv-import/main.rhai");
670 + let script_no_parse = r#"
671 + fn describe() {
672 + #{ name: "Broken", file_extensions: ["csv"] }
673 + }
674 + "#;
675 + std::fs::write(&script_path, script_no_parse).unwrap();
676 +
677 + let result = registry.reload_plugin("csv-import");
678 + assert!(result.is_err());
679 +
680 + match result.unwrap_err() {
681 + PluginError::MissingFunction { plugin, function } => {
682 + assert_eq!(plugin, "csv-import");
683 + assert_eq!(function, "parse");
684 + }
685 + other => panic!("Expected MissingFunction, got {:?}", other),
686 + }
687 + }
688 +
689 + // --- preview_import error paths ---
690 +
691 + #[test]
692 + fn preview_import_plugin_not_found() {
693 + let temp_dir = TempDir::new().unwrap();
694 + let registry = PluginRegistry::new(temp_dir.path()).unwrap();
695 +
696 + let result = registry.preview_import(
697 + "nonexistent",
698 + "/tmp/test.csv",
699 + ImportOptions::default(),
700 + Vec::new(),
701 + );
702 + assert!(result.is_err());
703 +
704 + match result.unwrap_err() {
705 + PluginError::PluginNotFound(id) => assert_eq!(id, "nonexistent"),
706 + other => panic!("Expected PluginNotFound, got {:?}", other),
707 + }
708 + }
709 +
710 + #[test]
711 + fn preview_import_rejects_non_import_plugin() {
712 + let temp_dir = TempDir::new().unwrap();
713 + create_command_plugin(temp_dir.path());
714 +
715 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
716 + registry.enable_plugin("my-command").unwrap();
717 +
718 + let result = registry.preview_import(
719 + "my-command",
720 + "/tmp/test.csv",
721 + ImportOptions::default(),
722 + Vec::new(),
723 + );
724 + assert!(result.is_err());
725 +
726 + match result.unwrap_err() {
727 + PluginError::InvalidManifest(msg) => {
728 + assert!(msg.contains("not an import plugin"), "Unexpected message: {}", msg);
729 + }
730 + other => panic!("Expected InvalidManifest, got {:?}", other),
731 + }
732 + }
733 +
734 + // --- iter_loaded ---
735 +
736 + #[test]
737 + fn iter_loaded_empty_initially() {
738 + let temp_dir = TempDir::new().unwrap();
739 + let registry = PluginRegistry::new(temp_dir.path()).unwrap();
740 + assert_eq!(registry.iter_loaded().count(), 0);
741 + }
742 +
743 + #[test]
744 + fn iter_loaded_reflects_enabled_plugins() {
745 + let temp_dir = TempDir::new().unwrap();
746 + create_csv_import_plugin(temp_dir.path());
747 + create_json_import_plugin(temp_dir.path());
748 + create_command_plugin(temp_dir.path());
749 +
750 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
751 + registry.enable_plugin("csv-import").unwrap();
752 + registry.enable_plugin("json-import").unwrap();
753 + registry.enable_plugin("my-command").unwrap();
754 +
755 + let loaded: Vec<_> = registry.iter_loaded().collect();
756 + assert_eq!(loaded.len(), 3);
757 +
758 + let ids: Vec<&str> = loaded.iter().map(|(id, _)| id.as_str()).collect();
759 + assert!(ids.contains(&"csv-import"));
760 + assert!(ids.contains(&"json-import"));
761 + assert!(ids.contains(&"my-command"));
762 + }
763 +
764 + #[test]
765 + fn iter_loaded_shrinks_after_disable() {
766 + let temp_dir = TempDir::new().unwrap();
767 + create_csv_import_plugin(temp_dir.path());
768 + create_json_import_plugin(temp_dir.path());
769 +
770 + let mut registry = PluginRegistry::new(temp_dir.path()).unwrap();
771 + registry.enable_plugin("csv-import").unwrap();
772 + registry.enable_plugin("json-import").unwrap();
773 + assert_eq!(registry.iter_loaded().count(), 2);
774 +
775 + registry.disable_plugin("csv-import").unwrap();
776 + assert_eq!(registry.iter_loaded().count(), 1);
777 +
778 + let remaining: Vec<_> = registry.iter_loaded().collect();
779 + assert_eq!(remaining[0].0, "json-import");
780 + }
781 +
782 + // --- initialize ---
783 +
784 + #[test]
785 + fn initialize_discovers_enabled_plugins() {
786 + let temp_dir = TempDir::new().unwrap();
787 + create_csv_import_plugin(temp_dir.path());
788 +
789 + // Manually create symlink to simulate a previously-enabled plugin
790 + let available = temp_dir.path().join("available/csv-import");
791 + let enabled = temp_dir.path().join("enabled/csv-import");
792 + std::fs::create_dir_all(temp_dir.path().join("enabled")).unwrap();
793 +
794 + #[cfg(unix)]
795 + std::os::unix::fs::symlink(&available, &enabled).unwrap();
796 +
Lines truncated
@@ -1,360 +0,0 @@
1 - # GoingsOn Architecture
2 -
3 - A Rust-based productivity application for independent workers managing projects, tasks, emails, and calendar events.
4 -
5 - ## High-Level Overview
6 -
7 - ```
8 - ┌─────────────────────────────────────────────────────────────┐
9 - │ User Interface │
10 - │ ┌────────────────────┐ ┌────────────────────────────────┐ │
11 - │ │ Tauri Desktop │ │ Axum Web Server │ │
12 - │ │ (Vanilla JS) │ │ (Askama Templates) │ │
13 - │ └─────────┬──────────┘ └──────────────┬─────────────────┘ │
14 - │ │ │ │
15 - │ ┌─────────▼──────────┐ ┌──────────────▼─────────────────┐ │
16 - │ │ Tauri Commands │ │ Axum API Routes │ │
17 - │ │ (src-tauri/) │ │ (web-server/src/api/) │ │
18 - │ └─────────┬──────────┘ └──────────────┬─────────────────┘ │
19 - └────────────┼────────────────────────────┼───────────────────┘
20 - │ │
21 - ┌────────────▼────────────────────────────▼───────────────────┐
22 - │ goingson-core │
23 - │ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────────┐ │
24 - │ │ Models │ │Repository│ │ Urgency │ │ Parser │ │
25 - │ │ │ │ Traits │ │ Calculator │ │ (Quick-Add) │ │
26 - │ └──────────┘ └────┬─────┘ └────────────┘ └──────────────┘ │
27 - └────────────────────┼────────────────────────────────────────┘
28 -
29 - ┌──────────────┴──────────────┐
30 - │ │
31 - ┌─────▼─────────────┐ ┌────────────▼──────────┐
32 - │ goingson-db- │ │ goingson-db- │
33 - │ sqlite │ │ postgres │
34 - │ (Desktop) │ │ (Web) │
35 - └───────────────────┘ └───────────────────────┘
36 - ```
37 -
38 - ## Workspace Structure
39 -
40 - ```
41 - goingson/
42 - ├── crates/
43 - │ ├── core/ # Domain models, traits, business logic
44 - │ ├── db-sqlite/ # SQLite repository implementations
45 - │ ├── db-postgres/ # PostgreSQL repository implementations
46 - │ └── web-server/ # Axum web server (multi-user)
47 - ├── src-tauri/ # Tauri desktop app (single-user)
48 - ├── migrations/
49 - │ ├── sqlite/ # SQLite schema migrations
50 - │ └── postgres/ # PostgreSQL schema migrations
51 - └── templates/ # Askama HTML templates (web)
52 - ```
53 -
54 - ## Crate Dependencies
55 -
56 - ```
57 - goingson-desktop (src-tauri)
58 - └── goingson-db-sqlite
59 - └── goingson-core
60 -
61 - goingson-web-server
62 - ├── goingson-db-postgres
63 - │ └── goingson-core
64 - └── goingson-core (for shared types)
65 - ```
66 -
67 - ## Core Crate (`crates/core/`)
68 -
69 - The core crate defines domain models and repository traits, independent of persistence.
70 -
71 - ### Modules
72 -
73 - | Module | Purpose |
74 - |--------|---------|
75 - | `models.rs` | Domain types: Project, Task, Event, Email, User, LlmSettings |
76 - | `repository.rs` | Repository traits (data access contracts) |
77 - | `urgency.rs` | TaskWarrior-inspired urgency calculation algorithm |
78 - | `parser.rs` | Quick-add natural language parser |
79 - | `recurrence.rs` | Task/event recurrence logic |
80 - | `validation.rs` | Input validation trait |
81 - | `constants.rs` | Named constants for thresholds, formats |
82 - | `error.rs` | Unified CoreError type |
83 -
84 - ### Key Types
85 -
86 - ```rust
87 - // Domain entities
88 - Project, Task, Event, Email, User, EmailAccount
89 - SavedView, Annotation, Subtask
90 -
91 - // Enums with display/parse support
92 - ProjectType, ProjectStatus, TaskStatus, Priority, Recurrence
93 -
94 - // DTOs for creation/updates
95 - NewProject, NewTask, NewEvent, NewEmail, UpdateTask
96 -
97 - // Repository traits
98 - ProjectRepository, TaskRepository, EventRepository
99 - EmailRepository, EmailAccountRepository, UserRepository
100 - SearchRepository, StatsRepository, SavedViewRepository
101 - LlmSettingsRepository, LlmCacheRepository
102 - ```
103 -
104 - ## Database Layer
105 -
106 - ### SQLite (`crates/db-sqlite/`)
107 -
108 - Used by the desktop app. Single-user, local storage.
109 -
110 - ```
111 - src/
112 - ├── lib.rs # SQLite pool initialization
113 - ├── utils.rs # format_datetime, parse_uuid, email validation
114 - └── repository/
115 - ├── mod.rs # Re-exports all repositories
116 - ├── project_repo.rs
117 - ├── task_repo.rs
118 - ├── event_repo.rs
119 - ├── email_repo.rs
120 - ├── email_account_repo.rs
121 - ├── user_repo.rs
122 - ├── search_repo.rs # FTS5 full-text search
123 - ├── stats_repo.rs # Dashboard aggregations
124 - ├── saved_view_repo.rs
125 - └── llm_repo.rs # LLM settings + response cache
126 - ```
127 -
128 - ### PostgreSQL (`crates/db-postgres/`)
129 -
130 - Used by the web server. Multi-user with sessions.
131 -
132 - Similar structure to SQLite, with PostgreSQL-specific features:
133 - - tsvector for full-text search
134 - - Session store integration (tower-sessions-sqlx-store)
135 -
136 - ## Tauri Desktop App (`src-tauri/`)
137 -
138 - Single-user desktop application.
139 -
140 - ```
141 - src/
142 - ├── main.rs # Tauri app setup, command registration
143 - ├── state.rs # AppState with repository instances
144 - ├── notifications.rs # Snooze watcher, native notifications
145 - ├── email/ # IMAP/SMTP client
146 - ├── llm/ # LLM provider clients (Ollama, OpenAI)
147 - └── commands/
148 - ├── mod.rs # Re-exports all commands
149 - ├── task.rs # Task CRUD, annotations, subtasks
150 - ├── email.rs # Email CRUD, IMAP sync, SMTP send
151 - ├── event.rs # Calendar events
152 - ├── project.rs # Project management
153 - ├── search.rs # Full-text search
154 - ├── stats.rs # Dashboard statistics
155 - ├── day_planning.rs # Time blocking
156 - ├── saved_views.rs # Custom filter views
157 - ├── llm.rs # LLM settings, template evaluation
158 - └── window.rs # Window management
159 - ```
160 -
161 - ### Command Pattern
162 -
163 - Tauri commands are async functions that:
164 - 1. Accept `State<Arc<AppState>>` for repository access
165 - 2. Deserialize input from frontend via `#[serde(rename_all = "camelCase")]`
166 - 3. Call repository methods
167 - 4. Serialize response types back to frontend
168 -
169 - ```rust
170 - #[tauri::command]
171 - pub async fn create_task(
172 - state: State<'_, Arc<AppState>>,
173 - input: TaskInput,
174 - ) -> Result<TaskResponse, String> {
175 - state.tasks
176 - .create(DESKTOP_USER_ID, new_task)
177 - .await
178 - .map(TaskResponse::from)
179 - .map_err(|e| e.to_string())
180 - }
181 - ```
182 -
183 - ## Web Server (`crates/web-server/`)
184 -
185 - Multi-user web application with Axum.
186 -
187 - ```
188 - src/
189 - ├── main.rs # Server setup, routing
190 - ├── api/ # REST API routes
191 - ├── auth/ # Session management, login/register
192 - └── email/ # IMAP/SMTP (shared with desktop)
193 - ```
194 -
195 - ## Frontend Architecture (Tauri Desktop)
196 -
197 - The desktop frontend uses vanilla JavaScript organized under the `GoingsOn` global namespace.
198 -
199 - ### Namespace Organization
200 -
201 - ```
202 - window.GoingsOn = {
203 - api: { ... }, // Tauri IPC abstraction layer
204 - state: { ... }, // Centralized state with pub/sub
205 - ui: { ... }, // Modal, toast, form utilities
206 - utils: { ... }, // HTML escaping, validation
207 -
208 - // Domain modules (IIFE-wrapped)
209 - projects: { ... },
210 - tasks: { ... },
211 - events: { ... },
212 - emails: { ... },
213 -
214 - // Feature modules
215 - savedViews: { ... },
216 - snooze: { ... },
217 - navigation: { ... },
218 - settings: { ... },
219 - app: { ... },
220 -
221 - // Infrastructure
222 - VirtualScroller, // Virtual scrolling for large lists
223 - SelectionManager, // Multi-select with shift/ctrl
224 - PaginationManager, // Page navigation
225 - };
226 - ```
227 -
228 - ### Module Pattern
229 -
230 - Each domain module is wrapped in an IIFE and exposes its public API through the namespace:
231 -
232 - ```javascript
233 - (function() {
234 - 'use strict';
235 - // Private state and helpers
236 - async function load() { ... }
237 - function openNew() { ... }
238 -
239 - // Public API
240 - GoingsOn.myModule = { load, openNew };
241 - })();
242 - ```
243 -
244 - ### Pre-computed Response Fields
245 -
246 - Rust response types include pre-computed display values so JS never calculates dates, formatting, or derived state:
247 -
248 - | Response Type | Pre-computed Fields |
249 - |--------------|---------------------|
250 - | TaskResponse | `dueFormatted`, `urgencyClass`, `isOverdue`, `isSnoozed`, `subtaskCount`, `subtaskCompleted`, `subtaskProgress` |
251 - | EventResponse | `timeFormatted`, `dateFormatted`, `isPast`, `proximityClass`, `proximityLabel` |
252 - | EmailResponse | `receivedFormatted` |
253 - | EmailAccountResponse | `lastSyncFormatted` |
254 -
255 - ### Centralized State
256 -
257 - All shared data lives in `GoingsOn.state` with reactive pub/sub:
258 -
259 - ```javascript
260 - GoingsOn.state.set('tasks', updatedTasks); // Triggers subscribers
261 - GoingsOn.state.subscribe('tasks', (newVal, oldVal) => { ... });
262 - ```
263 -
264 - ### File Organization
265 -
266 - ```
267 - src-tauri/frontend/
268 - ├── css/
269 - │ └── styles.css # Design system + all components
270 - ├── fonts/
271 - │ └── Reglo-Bold.woff2 # Display font
272 - ├── js/
273 - │ ├── goingson.js # Namespace root (window.GoingsOn)
274 - │ ├── api.js # Tauri IPC abstraction
275 - │ ├── state.js # Centralized state + pub/sub
276 - │ ├── utils.js # Escaping, validation, debounce
277 - │ ├── components.js # Modal, toast, form modal, confirm dialog
278 - │ ├── navigation.js # View switching, sidebar
279 - │ ├── tasks.js # Task list, CRUD, rendering
280 - │ ├── projects.js # Project list, detail, CRUD
281 - │ ├── events.js # Event list, CRUD, rendering
282 - │ ├── emails.js # Email list, threading, CRUD
283 - │ ├── day-planning.js # Time-blocking day planner
284 - │ ├── weekly-review.js # Weekly review workflow
285 - │ ├── saved-views.js # Custom filter views
286 - │ ├── snooze.js # Snooze modal + actions
287 - │ ├── settings.js # Settings, LLM config, export
288 - │ ├── import.js # Data import from JSON
289 - │ ├── bulk-actions.js # Multi-select bulk operations
290 - │ ├── context-menus.js # Right-click context menus
291 - │ ├── keyboard.js # Keyboard shortcuts
292 - │ ├── selection-manager.js # Multi-select with shift/ctrl
293 - │ ├── pagination-manager.js# Page navigation
294 - │ ├── virtual-scroller.js # Virtual scrolling for large lists
295 - │ ├── llm-templates.js # LLM template evaluation
296 - │ ├── seed-data.js # Demo data seeding
297 - │ └── app.js # App initialization, menu listeners
298 - └── index.html # Entry point
299 - ```
300 -
301 - ## Data Flow
302 -
303 - ### Desktop (Tauri)
304 - ```
305 - Frontend (JS)
306 - → invoke("command_name", { args })
307 - → Tauri IPC
308 - → commands/module.rs
309 - → Repository trait method
310 - → SQLite query
311 - → Response (with pre-computed display fields) → Frontend
312 - → JS renders pre-computed values directly to DOM
313 - ```
314 -
315 - ### Web (Axum)
316 - ```
317 - Browser
318 - → HTTP POST /api/tasks
319 - → Axum route handler
320 - → Repository trait method
321 - → PostgreSQL query
322 - → JSON response → Browser
323 - ```
324 -
325 - ## Key Design Decisions
326 -
327 - ### Clean Architecture
328 - - Core domain models have no dependencies on persistence
329 - - Repository traits define contracts, implementations are separate crates
330 - - Easy to swap databases or add new ones
331 -
332 - ### Single Codebase, Multiple Targets
333 - - Shared core logic between desktop and web
334 - - Desktop uses SQLite (local, offline-capable)
335 - - Web uses PostgreSQL (multi-user, hosted)
336 -
337 - ### Vanilla Frontend
338 - - No JavaScript framework — vanilla JS with IIFE modules
339 - - All code under `GoingsOn` global namespace (no `window.*` exports)
340 - - Centralized state via `GoingsOn.state` with pub/sub reactivity
341 - - IPC via Tauri invoke (desktop) or fetch (web)
342 - - Virtual scrolling for large lists (`GoingsOn.VirtualScroller`)
343 - - Optimized for desktop-class performance
344 -
345 - ### TaskWarrior-Inspired Features
346 - - Urgency calculation algorithm
347 - - Quick-add parser with natural language
348 - - Annotations and recurring tasks
349 -
350 - ## Testing Strategy
351 -
352 - - Unit tests in core crate for business logic
353 - - Integration tests for repository implementations (pending)
354 - - E2E tests via Tauri's testing framework (planned)
355 -
356 - ## Future Considerations
357 -
358 - - Structured logging with `tracing` crate
359 - - CalDAV for calendar sync
360 - - Multi-device sync strategy
@@ -1,162 +0,0 @@
1 - # MCP Integration Test
2 -
3 - Test the MCP server tools and GUI change detection after restarting Claude Code.
4 -
5 - ## Prerequisites
6 -
7 - 1. GoingsOn desktop app is running (`cargo tauri dev` or `cargo run` from `src-tauri/`)
8 - 2. MCP config exists at `/Users/max/Git/goingson/.mcp.json` (project-level, NOT `~/.claude/mcp.json`)
9 - 3. Claude Code was restarted AFTER the `.mcp.json` file was created
10 -
11 - ## Quick Verification
12 -
13 - Before running the tests, confirm the MCP tools are available. You should see tools like
14 - `get_context`, `list_tasks`, `create_task`, etc. in your tool list. If you don't see them:
15 -
16 - - The config is at `/Users/max/Git/goingson/.mcp.json` pointing to the built binary at
17 - `/Users/max/Git/goingson/target/debug/goingson-mcp`
18 - - If the binary is missing, rebuild with: `cargo build -p goingson-mcp`
19 - - Restart Claude Code after any config changes
20 -
21 - ## MCP Server Details
22 -
23 - - **Transport:** stdio (stdin/stdout JSON-RPC)
24 - - **Binary:** `/Users/max/Git/goingson/target/debug/goingson-mcp`
25 - - **Crate source:** `crates/goingson-mcp/`
26 - - **Database:** `~/Library/Application Support/com.goingson.desktop/goingson.db` (shared with desktop app)
27 - - **Framework:** `rmcp` crate with `transport-io` feature
28 -
29 - ## Test 1: Basic MCP Tools
30 -
31 - Run these commands in Claude Code to verify the MCP server is working:
32 -
33 - ```
34 - # List existing tasks
35 - list_tasks
36 -
37 - # List projects
38 - list_projects
39 -
40 - # Get current context (overdue, in-progress, etc.)
41 - get_context
42 - ```
43 -
44 - ## Test 2: Create Project via MCP
45 -
46 - ```
47 - # Create a test project
48 - create_project name:"MCP Test Project" description:"Project created via MCP integration test" project_type:SideProject
49 -
50 - # Verify it appears in the app's Projects view
51 - ```
52 -
53 - **Expected:** The project should appear in the Projects list without manually refreshing.
54 -
55 - ## Test 3: Create Task via MCP
56 -
57 - With the GoingsOn app open to the Tasks view:
58 -
59 - ```
60 - # Create a new task assigned to the project we just created
61 - create_task description:"Test task from MCP" priority:high project:"MCP Test Project"
62 -
63 - # Verify it appears in the app (should auto-refresh)
64 - ```
65 -
66 - **Expected:** The task should appear in the Tasks list under "MCP Test Project" without manually refreshing.
67 -
68 - ## Test 4: Update Task via MCP
69 -
70 - ```
71 - # First, list tasks to get an ID
72 - list_tasks
73 -
74 - # Update a task (use an actual ID from above)
75 - update_task id:<task-id> description:"Updated via MCP" priority:low
76 -
77 - # Verify the change appears in the app
78 - ```
79 -
80 - ## Test 5: Complete Task via MCP
81 -
82 - ```
83 - # Complete a task
84 - complete_task id:<task-id>
85 -
86 - # Verify it moves to completed in the app
87 - ```
88 -
89 - ## Test 6: Snooze Task via MCP
90 -
91 - ```
92 - # Snooze a task until tomorrow
93 - snooze_task id:<task-id> until:tomorrow
94 -
95 - # Verify it disappears from active tasks (check Snoozed filter)
96 - ```
97 -
98 - ## Test 7: Search
99 -
100 - ```
101 - # Search for tasks
102 - search query:"test"
103 - ```
104 -
105 - ## Test 8: Subtask Linking
106 -
107 - ```
108 - # Create two related tasks
109 - create_task description:"Phase 1: Design" project:GoingsOn
110 - create_task description:"Phase 2: Implement" project:GoingsOn
111 -
112 - # Link Phase 2 as a subtask of Phase 1
113 - add_subtask_link task_id:<phase1-id> linked_task_id:<phase2-id>
114 -
115 - # Open Phase 1 in the app and check subtasks modal
116 - ```
117 -
118 - ## Test 9: Export Roadmap
119 -
120 - ```
121 - # Generate a markdown roadmap for a project
122 - export_roadmap format:markdown project:GoingsOn
123 - ```
124 -
125 - ## Test 10: Delete Task via MCP
126 -
127 - ```
128 - # Delete a test task
129 - delete_task id:<task-id>
130 -
131 - # Verify it disappears from the app
132 - ```
133 -
134 - ## Troubleshooting
135 -
136 - **MCP tools not appearing in Claude Code:**
137 - - Config is at `/Users/max/Git/goingson/.mcp.json` (project-level)
138 - - Binary must exist at `/Users/max/Git/goingson/target/debug/goingson-mcp`
139 - - If binary is missing: `cargo build -p goingson-mcp`
140 - - Restart Claude Code after any config or binary changes
141 -
142 - **MCP server not responding:**
143 - - Check `.mcp.json` has the correct absolute path to `goingson-mcp`
144 - - Check stderr output for errors (the binary logs to stderr, MCP protocol uses stdout)
145 - - Restart Claude Code after config changes
146 -
147 - **GUI not auto-refreshing:**
148 - - Check the app console (Cmd+Option+I) for `db:external-change` events
149 - - Verify the db_watcher is running (check app logs for "Database watcher started")
150 - - The db_watcher uses a 500ms debounce + 1000ms min interval
151 -
152 - **Tasks not appearing:**
153 - - Both MCP server and desktop app use `~/Library/Application Support/com.goingson.desktop/goingson.db` on macOS
154 - - If paths differ, they won't see each other's changes
155 - - The MCP server auto-runs migrations on startup; both sides should be in sync
156 -
157 - ## Cleanup
158 -
159 - After testing, delete any test tasks:
160 - ```
161 - delete_task id:<test-task-id>
162 - ```
@@ -1,576 +0,0 @@
1 - # GoingsOn Style Guide
2 -
3 - ## Design Language: Skeubrute
4 -
5 - GoingsOn uses **Skeubrute**, a design system that combines **strong neobrutalism** with **abstract skeuomorphism**.
6 -
7 - ### Core Philosophy
8 -
9 - - **Neobrutalism**: Bold 3px borders, hard-edged offset shadows (4px), high contrast
10 - - **Skeuomorphism**: Paper textures, embossed text effects, tactile button states, realistic depth
11 -
12 - The result is an interface that feels like physical paper and buttons while maintaining a bold, modern aesthetic.
13 -
14 - ---
15 -
16 - ## Color System
17 -
18 - ### Background Colors
19 -
20 - | Variable | Hex | Usage |
21 - |----------|-----|-------|
22 - | `--bg-primary` | `#E8F4F8` | Page background |
23 - | `--bg-secondary` | `#D4EBF2` | Secondary surfaces, hover states |
24 - | `--bg-tertiary` | `#C0E2EC` | Tertiary surfaces |
25 - | `--bg-card` | `#FFFFFF` | Cards, modals, inputs |
26 -
27 - ### Text Colors
28 -
29 - | Variable | Hex | Usage |
30 - |----------|-----|-------|
31 - | `--text-primary` | `#1B365D` | Headings, primary text |
32 - | `--text-secondary` | `#3D5A80` | Body text, descriptions |
33 - | `--text-muted` | `#6B8CAE` | Captions, hints, disabled |
34 -
35 - ### Accent Colors
36 -
37 - | Variable | Hex | Usage |
38 - |----------|-----|-------|
39 - | `--accent-yellow` | `#F7D154` | Primary actions, active states, focus |
40 - | `--accent-green` | `#5CB85C` | Success, active status, tasks |
41 - | `--accent-blue` | `#1B365D` | Borders, info, emails |
42 - | `--accent-purple` | `#7B68EE` | Recurrence, essays, special |
43 - | `--accent-red` | `#DC3545` | Errors, high priority, warnings |
44 - | `--accent-cyan` | `#17A2B8` | Info, side projects, completed |
45 -
46 - ---
47 -
48 - ## Logo
49 -
50 - ### Full Logo
51 -
52 - The GoingsOn wordmark uses **Reglo Bold** with the following specifications:
53 -
54 - - **Font**: Reglo Bold
55 - - **Color**: `--text-primary` (#1B365D) on light backgrounds
56 - - **Alternate**: White on dark backgrounds
57 - - **Letter-spacing**: -0.02em (slightly tightened)
58 -
59 - ### Small Logo (Icon)
60 -
61 - The compact logo displays **"GO"** in Reglo Bold, centered within a neobrutalist container:
62 -
63 - ```
64 - ┌─────────────────┐
65 - │ │
66 - │ GO │
67 - │ │
68 - └─────────────────┘
69 - ```
70 -
71 - **Specifications:**
72 - - **Background**: `--accent-yellow` (#F7D154)
73 - - **Text**: `--text-primary` (#1B365D)
74 - - **Border**: 3px solid `--border-color` (#1B365D)
75 - - **Border Radius**: `--radius-sm` (6px)
76 - - **Shadow**: 2px 2px 0 `--border-color` (neobrutalist offset)
77 -
78 - **Files:**
79 - - `media/logo-go.svg` - Small "GO" icon logo
80 - - `media/logo-goingson.svg` - Full wordmark (future)
81 -
82 - ### Usage Guidelines
83 -
84 - | Context | Logo | Min Size |
85 - |---------|------|----------|
86 - | App icon / Favicon | GO icon | 16x16px |
87 - | Sidebar / Header | GO icon | 32x32px |
88 - | Splash / Marketing | Full wordmark | 120px wide |
89 - | Documentation | Either | Context-dependent |
90 -
91 - ---
92 -
93 - ## Typography
94 -
95 - ### Font Families
96 -
97 - ```css
98 - --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
99 - --font-serif: Georgia, 'Times New Roman', serif;
100 - --font-mono: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
101 - --font-display: 'Reglo', var(--font-serif);
102 - ```
103 -
104 - ### Display Font: Reglo
105 -
106 - The **Reglo** font is used for the logo and prominent H1-style headings. Reglo is an open-source display font with a bold, geometric character that complements the neobrutalist aesthetic.
107 -
108 - - **Source**: [Reglo by Sebastien Sanfilippo](https://github.com/nicokant/reglo) (OFL license)
109 - - **Usage**: Logo wordmark "GoingsOn", hero headings, splash screens
110 - - **Weights**: Bold only (display use)
111 -
112 - ```css
113 - @font-face {
114 - font-family: 'Reglo';
115 - src: url('fonts/Reglo-Bold.woff2') format('woff2');
116 - font-weight: 700;
117 - font-display: swap;
118 - }
119 - ```
120 -
121 - ### Semantic Aliases
122 -
123 - - `--font-display`: Uses `Reglo` for logo and hero headings
124 - - `--font-heading`: Uses `--font-serif` for titles and headings
125 - - `--font-body`: Uses `--font-sans` for body text
126 -
127 - ### Type Scale
128 -
129 - | Element | Size | Weight | Font |
130 - |---------|------|--------|------|
131 - | Page Title | 1.75rem | 700 | Serif |
132 - | Card Title | 1.1rem | 700 | Serif |
133 - | Modal Title | 1.25rem | 700 | Serif |
134 - | Body | 1rem | 400 | Sans |
135 - | Small | 0.875rem | 400 | Sans |
136 - | Caption | 0.75rem | 600 | Sans |
137 -
138 - ---
139 -
140 - ## Spacing
141 -
142 - The spacing system uses a consistent rem-based scale:
143 -
144 - | Name | Value | Usage |
145 - |------|-------|-------|
146 - | xs | 0.25rem | Badge padding, tight gaps |
147 - | sm | 0.5rem | Small gaps, icon spacing |
148 - | md | 0.75rem | Standard padding |
149 - | lg | 1rem | Section padding |
150 - | xl | 1.25rem | Card padding |
151 - | 2xl | 1.5rem | Page margins |
152 -
153 - ---
154 -
155 - ## Border & Shadow System
156 -
157 - ### Border Widths
158 -
159 - | Element | Width |
160 - |---------|-------|
161 - | Cards, Buttons, Modals | 3px (`--border-width`) |
162 - | Inputs, Badges, Tags | 2px |
163 - | Dividers | 2px |
164 -
165 - ### Border Radius
166 -
167 - | Variable | Value | Usage |
168 - |----------|-------|-------|
169 - | `--radius-sm` | 6px | Buttons, badges, inputs |
170 - | `--radius-md` | 12px | Cards, filter bars |
171 - | `--radius-lg` | 16px | Modals |
172 -
173 - ### Shadow System
174 -
175 - ```css
176 - /* Shadow utilities */
177 - .shadow-sm { box-shadow: 2px 2px 0 var(--border-color); }
178 - .shadow-md { box-shadow: 4px 4px 0 var(--border-color); } /* Default */
179 - .shadow-lg { box-shadow: 6px 6px 0 var(--border-color); }
180 - .shadow-xl { box-shadow: 8px 8px 0 var(--border-color); } /* Modals */
181 - .shadow-none { box-shadow: none; }
182 - ```
183 -
184 - ### Offset Shadow Values
185 -
186 - | Element | Shadow Offset |
187 - |---------|---------------|
188 - | Cards | 4px (with stacked paper effect) |
189 - | Buttons | 4px |
190 - | Modals | 8px |
191 - | Small buttons | 2-3px |
192 - | Inputs | Inset deboss |
193 -
194 - ---
195 -
196 - ## Skeuomorphic Effects
197 -
198 - ### Paper Texture
199 -
200 - Cards have a subtle SVG noise pattern for a paper-like feel:
201 -
202 - ```css
203 - .card {
204 - background-image: var(--texture-paper);
205 - }
206 - ```
207 -
208 - ### Embossed Text
209 -
210 - Headings use subtle text shadows to create a pressed-in effect:
211 -
212 - ```css
213 - .card-title, .modal-title, .page-title {
214 - text-shadow: var(--emboss-light), var(--emboss-dark);
215 - }
216 - ```
217 -
218 - Where:
219 - - `--emboss-light`: `1px 1px 0 rgba(255, 255, 255, 0.5)`
220 - - `--emboss-dark`: `-1px -1px 0 rgba(0, 0, 0, 0.08)`
221 -
222 - ### Debossed Inputs
223 -
224 - Form inputs have an inset shadow for a carved-in feel:
225 -
226 - ```css
227 - .form-input {
228 - box-shadow: var(--deboss);
229 - }
230 - /* --deboss: inset 1px 1px 2px rgba(0,0,0,0.08), inset -1px -1px 0 rgba(255,255,255,0.5) */
231 - ```
232 -
233 - ### Tactile Buttons
234 -
235 - Buttons have a gradient overlay for a 3D, pressable feel:
236 -
237 - ```css
238 - .btn {
239 - background-image: var(--btn-gradient);
240 - }
241 - .btn:active {
242 - background-image: var(--btn-gradient-pressed);
243 - }
244 - ```
245 -
246 - ---
247 -
248 - ## Components
249 -
250 - ### Buttons
251 -
252 - ```html
253 - <!-- Primary button (yellow background) -->
254 - <button class="btn btn-primary">Action</button>
255 -
256 - <!-- Secondary button (light background) -->
257 - <button class="btn btn-secondary">Cancel</button>
258 -
259 - <!-- Small button -->
260 - <button class="btn btn-sm">Small</button>
261 - ```
262 -
263 - **States:**
264 - - **Default**: 4px offset shadow
265 - - **Hover**: Lifts up (-2px, -2px), shadow increases
266 - - **Active/Pressed**: Pushes down (2px, 2px), shadow disappears
267 -
268 - ### Cards
269 -
270 - ```html
271 - <div class="card">
272 - <div class="card-header">
273 - <h3 class="card-title">Card Title</h3>
274 - </div>
275 - <p class="card-description">Description text</p>
276 - <div class="card-meta">
277 - <span class="tag type-job">Job</span>
278 - <span class="tag status-active">Active</span>
279 - </div>
280 - </div>
281 - ```
282 -
283 - Cards have:
284 - - Paper texture background
285 - - Stacked paper shadow effect (two-layer shadow)
286 - - Lift on hover
287 -
288 - ### Badges & Tags
289 -
290 - **Using data attributes (preferred):**
291 -
292 - ```html
293 - <span class="badge" data-color="green">Success</span>
294 - <span class="badge" data-color="yellow">Warning</span>
295 - <span class="badge" data-color="red">Error</span>
296 - <span class="badge" data-color="cyan">Info</span>
297 - <span class="badge" data-color="purple">Special</span>
298 - <span class="badge" data-color="muted">Default</span>
299 - ```
300 -
301 - **Legacy classes:**
302 -
303 - ```html
304 - <span class="tag type-job">Job</span>
305 - <span class="tag type-sideproject">Side Project</span>
306 - <span class="tag status-active">Active</span>
307 - <span class="tag status-completed">Completed</span>
308 - ```
309 -
310 - ### Form Inputs
311 -
312 - ```html
313 - <div class="form-group">
314 - <label class="form-label">Label</label>
315 - <input type="text" class="form-input" placeholder="Enter text...">
316 - </div>
317 -
318 - <div class="form-group">
319 - <label class="form-label">Select</label>
320 - <select class="form-select">
321 - <option>Option 1</option>
322 - </select>
323 - </div>
324 -
325 - <div class="form-group">
326 - <label class="form-label">Textarea</label>
327 - <textarea class="form-textarea"></textarea>
328 - </div>
329 - ```
330 -
331 - **Focus state**: Yellow ring (3px) around the input
332 -
333 - ### Modals
334 -
335 - ```html
336 - <div class="modal-overlay">
337 - <div class="modal-container">
338 - <div class="modal-header">
339 - <h2 class="modal-title">Modal Title</h2>
340 - <button class="modal-close">&times;</button>
341 - </div>
342 - <div class="modal-content">
343 - <!-- Content here -->
344 - </div>
345 - </div>
346 - </div>
347 - ```
348 -
349 - Modals have:
350 - - 8px offset shadow
351 - - 16px border radius
352 - - Semi-transparent overlay
353 -
354 - ### Tables
355 -
356 - ```html
357 - <table class="task-table">
358 - <thead>
359 - <tr>
360 - <th>Column</th>
361 - </tr>
362 - </thead>
363 - <tbody>
364 - <tr>
365 - <td>Data</td>
366 - </tr>
367 - </tbody>
368 - </table>
369 - ```
370 -
371 - Features:
372 - - Uppercase, letter-spaced headers
373 - - Hover state on rows
374 - - Selected state (yellow background)
375 -
376 - ### Empty & Error States
377 -
378 - ```html
379 - <div class="empty-state">
380 - <div class="empty-state-icon">icon</div>
381 - <p class="empty-state-text">No items found</p>
382 - </div>
383 -
384 - <div class="error-state">
385 - Error message here
386 - </div>
387 - ```
388 -
389 - ---
390 -
391 - ## Animation Standards
392 -
393 - ### Timing
394 -
395 - | Type | Duration | Easing |
396 - |------|----------|--------|
397 - | Hover effects | 0.1s - 0.15s | ease |
398 - | Transform (lift/press) | 0.1s | ease |
399 - | Focus rings | instant | - |
400 -
401 - ### Hover Lift Effect
402 -
403 - ```css
404 - .hover-lift {
405 - transition: transform 0.15s ease, box-shadow 0.15s ease;
406 - }
407 - .hover-lift:hover {
408 - transform: translate(-2px, -2px);
409 - }
410 - .hover-lift:active {
411 - transform: translate(2px, 2px);
412 - }
413 - ```
414 -
415 - ---
416 -
417 - ## Accessibility
418 -
419 - ### Focus States
420 -
421 - All interactive elements have visible focus indicators:
422 -
423 - ```css
424 - .btn:focus-visible,
425 - .form-input:focus-visible {
426 - outline: 3px solid var(--accent-yellow);
427 - outline-offset: 2px;
428 - }
429 - ```
430 -
431 - ### Screen Reader Support
432 -
433 - Use `.sr-only` for visually hidden but accessible text:
434 -
435 - ```html
436 - <span class="sr-only">Screen reader text</span>
437 - ```
438 -
439 - ### Color Contrast
440 -
441 - All text colors meet WCAG AA standards against their backgrounds:
442 - - Primary text on card: 8.5:1
443 - - Secondary text on card: 5.2:1
444 - - Muted text on card: 3.8:1
445 -
446 - ---
447 -
448 - ## File Organization
449 -
450 - ```
451 - src-tauri/frontend/
452 - ├── css/
453 - │ └── styles.css # All styles (design system + components)
454 - ├── fonts/
455 - │ └── Reglo-Bold.woff2 # Display font
456 - ├── js/
457 - │ ├── goingson.js # Namespace root (window.GoingsOn)
458 - │ ├── api.js # Tauri IPC abstraction
459 - │ ├── state.js # Centralized state + pub/sub
460 - │ ├── utils.js # HTML escaping, validation, debounce
461 - │ ├── components.js # Modal, toast, form modal, confirm dialog
462 - │ ├── navigation.js # View switching, sidebar
463 - │ ├── tasks.js # Task list, CRUD, rendering
464 - │ ├── projects.js # Project list, detail view
465 - │ ├── events.js # Event list, CRUD
466 - │ ├── emails.js # Email list, threading
467 - │ ├── settings.js # Settings, LLM config, export
468 - │ └── app.js # App initialization, menu listeners
469 - └── index.html # Entry point (no inline styles)
470 - ```
471 -
472 - See `ARCHITECTURE.md` (in this folder) for the full JS file listing and namespace organization.
473 -
474 - ---
475 -
476 - ## CSS Naming Convention
477 -
478 - GoingsOn uses a simplified BEM-adjacent pattern with kebab-case.
479 -
480 - ### Pattern: `.block-element`
481 -
482 - ```
483 - .component → Block (card, modal, btn, form)
484 - .component-part → Element within block (card-header, modal-title)
485 - .component-modifier → Variant (btn-primary, btn-sm)
486 - ```
487 -
488 - ### Examples
489 -
490 - ```css
491 - /* Block */
492 - .card { }
493 - .modal { }
494 - .btn { }
495 -
496 - /* Elements (single hyphen) */
497 - .card-header { }
498 - .card-title { }
499 - .card-description { }
500 - .modal-overlay { }
Lines truncated
D docs/about.md -142
@@ -1,142 +0,0 @@
1 - # GoingsOn
2 -
3 - Native desktop productivity app for independent workers. Tasks, email, calendar, contacts, and weekly planning in one offline-first application.
4 -
5 - ## What It Is
6 -
7 - A Tauri 2 desktop app combining TaskWarrior-style task management with IMAP/SMTP email, calendar events, contacts, and weekly review — all in a single native app. Offline-first with local SQLite storage. No cloud dependency.
8 -
9 - **License:** PolyForm Noncommercial 1.0.0
10 -
11 - ## Tech Stack
12 -
13 - - **Framework:** Tauri 2.10.2 (Rust backend + Vanilla JS frontend)
14 - - **Database:** SQLite with sqlx 0.8
15 - - **Email:** async-imap 0.11, lettre 0.11 (IMAP/SMTP), JMAP for Fastmail
16 - - **OAuth:** Custom PKCE implementation for Fastmail, Google, Microsoft, Yahoo
17 - - **Security:** Argon2 0.5 (password hashing), keyring 3 (OS credential storage)
18 - - **Plugins:** Rhai 1.17 scripting engine
19 - - **Design:** "Skeubrute" aesthetic — paper texture, embossed UI, 10 built-in themes
20 -
21 - **Workspace:** 4 crates — `core` (domain models), `db-sqlite` (repository), `goingson-mcp` (Claude integration), `plugin-runtime` (Rhai plugins)
22 -
23 - ## Features
24 -
25 - ### Task Management
26 - - CRUD with 4 statuses (Pending, Started, Completed, Deleted) and 3 priority levels
27 - - TaskWarrior-inspired urgency scoring (priority, overdue penalty, due-soon scaling, age bonus, started bonus, urgent tag)
28 - - Recurrence (daily, weekly, monthly) with auto-creation on completion
29 - - Subtasks (ordered checkboxes), annotations (timestamped notes)
30 - - Due dates with smart natural language parsing
31 - - Snooze/defer until date, time blocking (scheduled_start + duration)
32 - - Milestone assignment and tracking
33 -
34 - ### Email Integration
35 - - Full IMAP/SMTP client with OAuth2 for 4 providers (Fastmail, Google, Microsoft, Yahoo)
36 - - JMAP support for Fastmail (native protocol)
37 - - XOAUTH2 authentication for IMAP and SMTP
38 - - Email threading with conversation grouping
39 - - Reader mode (auto-strips HTML for clean display)
40 - - Email-to-task conversion with source tracking
41 - - Auto-sync with configurable intervals (5/15/30/60 min)
42 - - Archive, read/unread tracking, unread count
43 -
44 - ### Calendar & Events
45 - - Full CRUD with title, description, location, time range
46 - - Event recurrence (daily, weekly, monthly)
47 - - Task-event auto-linking (events from due dates)
48 - - Time block types: Free Time, Personal, Vacation, Focus
49 - - Event reminders (configurable lead time)
50 - - Conflict detection with existing time blocks
51 - - Date proximity coloring (green=today, yellow=tomorrow, cyan=this week)
52 -
53 - ### Contacts
54 - - Full CRUD with email, phone, social handles, arbitrary custom fields
55 - - Full-text search across name, nickname, company, notes, tags
56 - - Auto-suggest contact from email sender
57 - - Contact linking to tasks, events, and email threads
58 -
59 - ### Weekly Review & Day Planning
60 - - Guided weekly review: past week stats, coming week planning
61 - - Task focus system (star weekly priorities, focused projects derived from tasks)
62 - - Monday nudge (tab badge + startup toast)
63 - - Week timeline with daily dots (completed, events, overdue)
64 - - Vacation day toggles with "Day Off" banners
65 - - Day view with paint-to-create scheduling modal
66 - - Time blocks with conflict detection
67 - - Collapsible sidebar with unscheduled tasks
68 -
69 - ### Search & Filtering
70 - - Full-text search via SQLite FTS5 across all entity types
71 - - Date range syntax (after:/before:/from:/to:)
72 - - Saved views / named filters with pinning
73 - - Multi-filter combining (AND logic)
74 -
75 - ### Quick-Add Parser
76 - - Natural language: `+tag`, `project:Name`, `priority:H/M/L`, `due:tomorrow`, `due:+3d`, `recur:weekly`
77 -
78 - ### Bulk Actions
79 - - Multi-select (Cmd+Click, Shift+Click range, Cmd+A)
80 - - Bulk archive, delete, snooze for tasks and emails
81 -
82 - ### Keyboard & UX
83 - - Vim-style navigation (j/k, g+t/e/p for tabs)
84 - - Quick-add from anywhere (q)
85 - - Native menu bar with standard shortcuts
86 - - Context menus, keyboard shortcut overlay (?)
87 - - Virtual scrolling for large lists
88 -
89 - ### Export & Backup
90 - - JSON export (all data), CSV export (tasks), ICS export (calendar)
91 - - Automated daily compressed backups with configurable retention
92 - - Markdown export (task lists)
93 -
94 - ### Plugin System (Rhai)
95 - - Plugin manifest format (plugin.toml)
96 - - CSV import reference plugin with preview UI
97 - - Plugin manager (enable/disable)
98 - - File watcher for hot-reload
99 -
100 - ### MCP Server (Claude Integration)
101 - - 16 tools: task CRUD (9), project CRUD (5), event CRUD (5), dashboard stats, search, context, roadmap export
102 - - SQLite connection to same DB as desktop app
103 - - Configured via ~/.claude/mcp.json
104 -
105 - ### LLM Integration
106 - - Ollama / OpenAI-compatible providers
107 - - Dynamic templates `{: prompt :}` and static templates `(: prompt :}` in form fields
108 - - AI-Fill button, context injection, response caching
109 -
110 - ### Desktop Native Features
111 - - System notifications (snooze expiry, overdue reminders, event reminders)
112 - - Menu bar icon (macOS — "GO" in Reglo)
113 - - Window size/position persistence
114 - - 10 themes: Neobrute, Catppuccin (3), Dracula, Nord, Tokyo Night, Flatwhite, Ayu Light + Follow System
115 -
116 - ## Testing
117 -
118 - - **205 tests** across codebase
119 - - Core crate: 64 tests (domain models, business logic)
120 - - DB-SQLite: 38 integration tests (repository operations)
121 - - Desktop commands: 17 tests (Tauri IPC handlers)
122 - - Plus unit tests in 32+ modules
123 - - Manual testing docs: `docs/human_testing.md`, `docs/MCP_test.md`
124 - - Demo data generator for manual testing (`seedDemoData()`, `clearAllData()`)
125 -
126 - ## Platform Support
127 -
128 - | Platform | Status |
129 - |----------|--------|
130 - | macOS | Primary, functional |
131 - | Windows | Supported via Tauri |
132 - | Linux | Supported via Tauri |
133 - | iOS | Building on simulator (iPhone 17 Pro, iOS 26.2) |
134 - | Android | Build infrastructure in place, testing pending |
135 -
136 - ## Status
137 -
138 - **Done:** All core features (tasks, projects, events, emails, contacts), weekly review, day planning, full email integration with OAuth2, plugin system, MCP server, 10 themes, mobile responsive CSS, iOS simulator builds.
139 -
140 - **Active:** Mobile port (iOS/Android), desktop polish.
141 -
142 - **Next:** macOS signing + notarization, Android build completion, cloud sync (SyncKit), Kanban view.
@@ -1,395 +0,0 @@
1 - # GoingsOn -- Audit Review
2 -
3 - **Last audited:** 2026-03-01 (fourth audit)
4 - **Previous audit:** 2026-02-28 (third audit)
5 - **Auditor:** Claude Opus 4.6 (automated codebase audit)
6 - **Scope:** Full workspace (`crates/core`, `crates/db-sqlite`, `crates/goingson-mcp`, `crates/plugin-runtime`, `src-tauri`, frontend JS, migrations)
7 -
8 - ---
9 -
10 - ## Overall Grade: A
11 -
12 - Production-ready codebase with excellent fundamentals. Theme system rewritten with multi-directory loading (bundled resources, dev fallback, user custom), AppHandle integration, is_custom field, high-contrast UI group. Zero clippy warnings. All tests pass. Two minor findings: missing `//!` docs on `smtp_client.rs`, `notify-debouncer-mini` not in workspace deps. Previous open items unchanged (`list_completed_between`, MCP tests).
13 -
14 - ---
15 -
16 - ## Scorecard
17 -
18 - | Dimension | Grade | Notes |
19 - |-----------|:-----:|-------|
20 - | **Code Quality** | A | Zero `.unwrap()` in non-test code (one `unwrap_or` fallback in recurrence.rs is safe). Consistent `CoreError`/`ApiError` chain with structured error codes. `expect()` only used for statically valid values and startup initialization. |
21 - | **Architecture** | A | Clean 5-crate workspace: domain (core) -> persistence (db-sqlite) -> plugins (plugin-runtime) -> MCP server (goingson-mcp) -> app (src-tauri). Repository trait pattern with async implementations. No layer violations. |
22 - | **Testing** | A- | 289 tests pass, 0 failures, 10 ignored. 163 unit tests in crates, 66 integration tests in db-sqlite (including 18 contact_repo + 12 search_repo), 42 command/export/watcher tests in src-tauri. 13 plugin-runtime tests. Good coverage on parsers, validation, recurrence, day planning, models, search, contacts. Gap: no tests for email sync integration or MCP server tools. |
23 - | **Security** | A | All SQL parameterized via sqlx bind. Dynamic SQL only for structural elements (column names from enums, never user input). FTS5 queries escaped via `prepare_fts5_query()`. Frontend uses `escapeHtml()`/`escapeAttr()` consistently (198 calls across 21 files, 54 innerHTML assignments all through escaping). Credentials in OS keychain. OAuth2 + PKCE. |
24 - | **Performance** | A- | Batch annotation/subtask fetches avoid N+1. Virtual scrolling on frontend. FTS5 for search. Pagination on tasks and email threads. Still: dashboard stats runs 7 sequential queries; search does client-side pagination across multiple entity types. |
25 - | **Documentation** | A | `//!` module docs on every Rust source file (all 17 repo files, all core modules, all command modules). `///` doc comments on all public types and methods. Doc examples with builder patterns that compile. `docs/ARCHITECTURE.md` and `docs/STYLEGUIDE.md` exist. Named constants with explanations. |
26 - | **Dependencies** | A | All deps pinned at workspace level in root `Cargo.toml`. Core crate has zero framework deps. Desktop-only deps gated with `cfg(not(mobile))`. Only external path dep is `synckit-client`. No unused deps detected. thiserror for all error types. |
27 - | **Frontend** | A- | Single `window.GoingsOn` namespace with 38 sub-namespaces. IIFE pattern with `'use strict'` on all modules. Centralized `AppStateManager` with pub/sub. `window.GO` alias. Minor: `window.__TAURI__` accessed in api.js (necessary), some `window.` references for event listeners (34 total, most necessary). No automated JS tests. |
28 - | **Type Safety** | A | 11 entity ID newtypes via `define_uuid_id!` macro with feature-gated sqlx impls. `ViewFilters.status`/`priority` now use typed enums. `SavedView.sort_by` uses `SortField` enum. `SortDirection::sql()` moved to db-sqlite. All model structs use typed IDs. |
29 - | **Observability** | A- | Structured `tracing` throughout with EnvFilter. All 5 background services use structured macros with key=value fields. Only 3 `#[instrument]` in 135+ Tauri commands. No request/trace ID correlation. No middleware for Tauri IPC request tracing. `reqwest::Client` instances lack request logging. |
30 - | **Concurrency** | A | SQLite serializes all writes. No shared mutable state -- `AppState` holds `Arc<dyn Repository>`. No `Mutex`/`RwLock` in application code. UNIQUE constraints on users.email, email_accounts, llm_settings, etc. Email sync processes accounts sequentially. `INSERT OR REPLACE` for idempotent upserts. |
31 - | **Resilience** | B+ | Background tasks continue when individual operations fail. `applying_remote` flag always cleared on error. Startup crash recovery. Only LLM client has explicit timeout; JMAP, OAuth, and IMAP clients use implicit defaults. No circuit breakers or retry strategies. No explicit `acquire_timeout` on SqlitePool. No graceful shutdown hook for flushing sync changelog. |
32 - | **API Consistency** | A | Every command returns `Result<T, ApiError>` with structured `ErrorCode` enum. `CoreError` -> `ApiError` covers all 8 variants. Consistent pagination via `PaginatedResponse<T>`. All response types use `camelCase`. Pre-computed display fields (is_snoozed, urgency_class, due_formatted). Extension traits standardize error conversion. |
33 - | **Migration Safety** | A- | All 31 migrations strictly additive (CREATE TABLE, ADD COLUMN, CREATE INDEX, FTS5, triggers). 65 IF NOT EXISTS guards on later migrations. One safe table recreation in 029 (SQLite column rename pattern). No DOWN migrations (acceptable for desktop SQLite). Early migrations (001-009) lack IF NOT EXISTS guards. |
34 - | **Codebase Size** | A | ~58K lines implementing 20+ feature domains (tasks, email, calendar, contacts, weekly review, search, plugins, MCP, sync, themes, etc.) with 433 tests. ~2,900 lines per major feature. No dead code, no bloat, no over-engineering. Pre-computed response pattern eliminates JS duplication. |
35 -
36 - ---
37 -
38 - ## Module Heatmap
39 -
40 - | Module | Code | Arch | Test | Security | Perf | Docs | Deps | Frontend |
41 - |--------|:----:|:----:|:----:|:--------:|:----:|:----:|:----:|:--------:|
42 - | **crates/core (models)** | A | A | A- | n/a | n/a | A | A | n/a |
43 - | **crates/core (parser)** | A | A | A+ | n/a | A | A | n/a | n/a |
44 - | **crates/core (validation)** | A | A | A | n/a | n/a | A | n/a | n/a |
45 - | **crates/core (urgency)** | A | A | A | n/a | A | A | n/a | n/a |
46 - | **crates/core (recurrence)** | A | A | A+ | n/a | A | A | n/a | n/a |
47 - | **crates/core (search_parser)** | A | A | A | n/a | A | A | n/a | n/a |
48 - | **crates/core (weekly_review)** | A | A | A- | n/a | B+ | A | n/a | n/a |
49 - | **crates/core (repository traits)** | A | A+ | n/a | n/a | n/a | A | n/a | n/a |
50 - | **crates/core (email_sync)** | A- | A | B+ | n/a | A- | A | n/a | n/a |
51 - | **crates/core (day_planning)** | A | A | A | n/a | A | A | n/a | n/a |
52 - | **crates/core (error)** | A | A | A | n/a | n/a | A | n/a | n/a |
53 - | **crates/db-sqlite (task_repo)** | A | A | A- | A | A- | A | n/a | n/a |
54 - | **crates/db-sqlite (email_repo)** | A | A | A- | A | A | A | n/a | n/a |
55 - | **crates/db-sqlite (search_repo)** | A- | A | A- | A | B+ | A | n/a | n/a |
56 - | **crates/db-sqlite (contact_repo)** | A | A | A- | A | A | A | n/a | n/a |
57 - | **crates/db-sqlite (stats_repo)** | A- | A | B | A | B | A | n/a | n/a |
58 - | **crates/db-sqlite (other repos)** | A | A | A- | A | A | A | n/a | n/a |
59 - | **crates/db-sqlite (utils)** | A | A | A | A | A | A | n/a | n/a |
60 - | **crates/plugin-runtime** | A- | A | A- | A- | A | A | A | n/a |
61 - | **crates/goingson-mcp** | A | A | B | A | A | A | A | n/a |
62 - | **src-tauri (commands)** | A | A | A- | A | A- | A | n/a | n/a |
63 - | **src-tauri (email/jmap)** | A- | A | B | A | A- | A- | n/a | n/a |
64 - | **src-tauri (oauth)** | A | A | B+ | A | n/a | A | n/a | n/a |
65 - | **src-tauri (sync_service)** | A- | A | B | A | A | A- | n/a | n/a |
66 - | **src-tauri (export)** | A | A | A- | A | A | A | n/a | n/a |
67 - | **src-tauri (state/main)** | A | A | n/a | A | A | A | n/a | n/a |
68 - | **frontend (goingson/state)** | n/a | A | n/a | n/a | n/a | A- | n/a | A |
69 - | **frontend (api)** | n/a | A | n/a | n/a | n/a | A | n/a | A |
70 - | **frontend (utils)** | n/a | A | n/a | A | n/a | A | n/a | A |
71 - | **frontend (tasks)** | n/a | A | n/a | A | A- | A- | n/a | A |
72 - | **frontend (emails)** | n/a | A | n/a | A | A- | A- | n/a | A |
73 - | **frontend (projects)** | n/a | A | n/a | A | A | A- | n/a | A |
74 - | **frontend (events)** | n/a | A | n/a | A | A | A- | n/a | A |
75 - | **frontend (day-planning)** | n/a | A | n/a | A | A | A- | n/a | A- |
76 - | **frontend (contacts)** | n/a | A | n/a | A | A | A- | n/a | A |
77 - | **frontend (weekly-review)** | n/a | A | n/a | A | A | A- | n/a | A |
78 - | **frontend (components)** | n/a | A | n/a | A | A | A- | n/a | A |
79 - | **frontend (settings)** | n/a | A- | n/a | A | A | A- | n/a | A |
80 - | **frontend (keyboard/touch)** | n/a | A | n/a | n/a | A | A- | n/a | A |
81 -
82 - ### Cold Spots
83 -
84 - Modules graded B or below, or 2+ letter grades below the project median for that dimension:
85 -
86 - - **crates/db-sqlite/stats_repo (Performance): B** -- 7 sequential DB queries for dashboard stats. Should be a single CTE query. Low severity for desktop (SQLite is fast locally) but matters for mobile.
87 - - **crates/db-sqlite/search_repo (Testing): A-** -- 12 integration tests covering FTS5 search, filters, pagination, edge cases. Remaining gap: no tests for email search (only tasks and projects tested).
88 - - **crates/db-sqlite/contact_repo (Testing): A-** -- 18 integration tests covering CRUD, sub-collections, filtering, find-by-email. Comprehensive coverage.
89 - - **crates/db-sqlite/stats_repo (Testing): B** -- No tests for dashboard stats computation. The stats_repo is only 82 lines but the queries are complex enough to warrant regression tests.
90 - - **crates/goingson-mcp (Testing): B** -- MCP server has no automated tests. The 16-tool implementation relies entirely on manual testing via Claude Code.
91 - - **src-tauri/email/jmap (Testing): B** -- JMAP implementation has no automated tests. Relies on manual testing with live Fastmail accounts.
92 - - **src-tauri/sync_service (Testing): B** -- Sync engine has no automated tests. The push/pull/apply cycle with FK-ordered upserts and deletes is complex enough to warrant at least unit tests on the ordering logic.
93 - - **crates/core/email_sync (Testing): B+** -- Only trivial struct construction tests. The `process_fetched_emails` function is tested only via the FetchedEmail struct test, not the actual dedup-save-clear logic.
94 - - **crates/db-sqlite/search_repo (Performance): B+** -- Cross-entity search collects all results in memory then paginates client-side. For large datasets this could be slow, though the per-entity limits mitigate it.
95 - - **crates/core/weekly_review (Performance): B+** -- `compute_project_health` iterates all tasks per project (O(projects * tasks)). Could use a HashMap pre-group for O(tasks + projects).
96 -
97 - ---
98 -
99 - ## Strengths
100 -
101 - ### 1. Zero unsafe patterns in production code
102 -
103 - Not a single `.unwrap()` call exists outside of `#[test]` and `#[cfg(test)]` blocks. The one `unwrap_or(dt)` in `recurrence.rs:47` is a safe fallback. All `expect()` calls are for statically valid values (`NaiveTime::from_hms_opt(17, 0, 0).expect("17:00 is valid")`) or startup initialization where failure is correctly fatal. The `CoreError` -> `ApiError` conversion chain is clean and typed, with structured error codes and optional field-level details the frontend can act on.
104 -
105 - ### 2. Comprehensive SQL injection and XSS prevention
106 -
107 - Every database query uses sqlx parameterized bind. Dynamic SQL is used in only 4 places (task_repo filter building, email_account select, search_repo), and in every case the dynamic parts come from Rust enums or string constants, never user input. The FTS5 search layer escapes special characters via `prepare_fts5_query()` before they reach the query. On the frontend, 198 calls to `escapeHtml()`/`escapeAttr()` across 21 JS files cover every dynamic value touching innerHTML. Email HTML bodies are stripped server-side and never rendered raw.
108 -
109 - ### 3. Clean 5-crate architecture with proper domain isolation
110 -
111 - The `core` crate (4,700 lines) has zero dependencies on SQLite, Tauri, or any framework -- only chrono, uuid, serde, thiserror, and domain logic. It defines 12 repository trait interfaces that `db-sqlite` (5,350 lines) implements. The `src-tauri` crate is a thin command layer (6,800 lines across 22 command modules) that maps Tauri IPC to repository calls with pre-computed response fields. The `plugin-runtime` and `goingson-mcp` crates are fully independent. Dependency direction is strictly acyclic: core <- db-sqlite <- src-tauri; core <- plugin-runtime; core + db-sqlite <- goingson-mcp.
112 -
113 - ### 4. Thorough validation and named constants
114 -
115 - Input validation via the `Validate` trait is centralized in `core/validation.rs` and enforced for `NewTask`, `UpdateTask`, `NewProject`, and `NewEvent`. All magic numbers are named constants in `core/constants.rs` with doc comments explaining their purpose and usage context. 17 validation tests cover empty strings, whitespace-only, length limits, negative durations, and boundary conditions.
116 -
117 - ### 5. Frontend namespace discipline
118 -
119 - 38 modules populate the `GoingsOn` namespace through IIFE patterns with `'use strict'`. Centralized state uses `AppStateManager` with pub/sub reactivity. Cross-module calls use `GoingsOn.moduleName.functionName()`. The `GoingsOn.handle()` dispatcher enables type-safe onclick routing without exposing individual functions to the global scope. API calls are abstracted through `GoingsOn.api` with one-to-one mapping to Tauri commands.
120 -
121 - ---
122 -
123 - ## Weaknesses
124 -
125 - ### 1. ~~`body_preview()` byte-slicing can panic on multi-byte UTF-8~~ (RESOLVED)
126 -
127 - Already uses `.chars().take(n)` with 18 unit tests covering multi-byte UTF-8 scenarios. The initial audit finding was incorrect.
128 -
129 - ### 2. `list_completed_between` ignores date parameters (Logic bug -- unfixed)
130 -
131 - **File:** `crates/db-sqlite/src/repository/task_repo.rs:755`
132 -
133 - Parameters are underscore-prefixed and unused. The weekly review's "completed this week" section returns the last 100 completed tasks regardless of date range, making it inaccurate. Requires adding a `completed_at` column.
134 -
135 - ### 3. Testing gaps in remaining modules
136 -
137 - Four modules with significant code (email/jmap, sync_service, MCP server tools, stats_repo) have zero or near-zero automated tests. The MCP server tools process task mutations from an external agent (Claude) and should have at least basic CRUD tests. Search_repo and contact_repo gaps have been addressed (12 + 18 tests respectively).
138 -
139 - ### 4. ~~Dashboard stats uses 7 sequential queries~~ (RESOLVED)
140 -
141 - Already uses a single query with 6 subqueries. The initial audit finding was incorrect.
142 -
143 - ---
144 -
145 - ## Competitive Comparison
146 -
147 - GoingsOn occupies a unique position as the only app combining tasks, email, calendar, contacts, and weekly review in a single offline-first native application. Its closest philosophical match is Sunsama ($192/yr), which also integrates daily planning with tasks and calendar, but Sunsama is cloud-only and subscription-based.
148 -
149 - **Key competitive advantages:**
150 - - Only app with all 5 domains integrated (tasks + email + calendar + contacts + weekly review)
151 - - Offline-first with zero cloud dependency (vs. Todoist, Notion, Sunsama which require internet)
152 - - Source-available under PolyForm Noncommercial (unique among all competitors)
153 - - MCP server with 16 tools for LLM agent integration (no competitor offers this)
154 - - Rhai plugin system for user extensibility (only Obsidian competes here)
155 - - TaskWarrior-style urgency algorithm (more sophisticated than any competitor's priority system)
156 - - No subscription fee (vs. $48-$408/yr for competitors)
157 - - Cross-platform including Linux (Things 3, Fantastical are Apple-only)
158 -
159 - **Key competitive gaps:**
160 - 1. **Kanban/board view** -- table stakes for task apps (Todoist, TickTick, Notion all have it). On the roadmap.
161 - 2. **Monthly calendar view** -- universally expected. Only day plan timeline exists.
162 - 3. **External calendar sync** -- Google Calendar, Apple Calendar, CalDAV. Planned, high priority.
163 - 4. **Mobile app** -- in progress (iOS simulator builds working), but competitors all have mature mobile apps.
164 - 5. **Guided daily planning ritual** -- Sunsama's signature feature. GoingsOn has weekly review but no structured daily workflow.
165 -
166 - **Code quality vs. competitors:** The codebase is architecturally ready to execute on these gaps. The repository trait pattern means a CalDAV sync backend is an implementation concern. Kanban is a frontend view over existing task data. The existing patterns (command layer, namespace discipline, escaping utilities) make new features straightforward to add without architectural changes.
167 -
168 - ---
169 -
170 - ## Action Items
171 -
172 - All resolved. Outstanding work tracked in `docs/todo/todo.md`.
173 -
174 - 1. ~~**[Bug]** Fix `body_preview()` UTF-8 panic~~ — already uses `.chars().take(n)` with 18 tests
175 - 2. ~~**[Performance]** Batch dashboard stats queries~~ — already uses single query with 6 subqueries
176 - 3. ~~**[Clippy]** Fix `clone_on_copy` in goingson-mcp~~ — fixed (`task_impl.rs:271`)
177 - 4. ~~**[Testing]** Add integration tests for `search_repo`~~ — 15 tests added
178 - 5. ~~**[Testing]** Add integration tests for `contact_repo`~~ — 18 tests added
179 - 6. ~~**[Logic bug]** `list_completed_between` date filtering~~ — `completed_at` column added (migration 031), 4 tests
180 - 7. ~~**[Testing]** MCP server tool tests~~ — 20 integration tests added (`crates/goingson-mcp/tests/mcp_tests.rs`)
181 -
182 - ---
183 -
184 - ## Changes Since Last Audit
185 -
186 - **Previous audit:** 2026-02-27 (first audit)
187 -
188 - ### What improved
189 - - No regressions detected. All 234 tests pass. `cargo check --workspace` clean.
190 - - The pre-audit fixes noted in the first review (recurrence calc moved to command layer, `snoozed_until_formatted` added, date math moved to Rust, dead code removed, module-local state fixed) remain in place and working.
191 -
192 - ### What regressed
193 - - Nothing. The three unfixed issues from the first audit (`body_preview` UTF-8, `list_completed_between`, stats batching) remain outstanding but were already tracked.
194 -
195 - ### New issues found
196 - - Testing coverage gaps identified at the module level (search_repo, contact_repo, MCP tools) -- these were present at the first audit but not called out at module granularity.
197 - - `compute_project_health` has O(projects * tasks) complexity in `weekly_review.rs` -- not a regression, just newly noted.
198 -
199 - ---
200 -
201 - ## Build Verification
202 -
203 - ```
204 - cargo check --workspace PASS
205 - cargo test --workspace 289 passed, 0 failed, 10 ignored
206 - cargo clippy --workspace 0 warnings
207 - ```
208 -
209 - Ignored tests (10 total):
210 - - 2x macOS keychain (`oauth::credentials::tests::test_oauth_roundtrip`, `test_password_roundtrip`)
211 - - 4x goingson-mcp unit tests (require MCP test framework)
212 - - 1x goingson-mcp doc test
213 - - 2x plugin-runtime tests
214 - - 1x plugin-runtime doc test
215 -
216 - ## Changes Since Last Audit (2026-02-28, second audit)
217 -
218 - ### Bug fixes
219 - - Fixed tag search bug in `search_repo.rs:319-337`: referenced nonexistent `task_tags` junction table, now uses `LIKE '%"tag"%'` on JSON array in `tasks.tags` column (matching `contact_repo` pattern)
220 - - Fixed `clone_on_copy` clippy warning in `goingson-mcp/src/tools/task_impl.rs:271`
221 -
222 - ### Tests added
223 - - 18 new contact_repo integration tests (`crates/db-sqlite/tests/contact_repo_tests.rs`): CRUD, sub-collections (emails, phones, social handles, custom fields), tag filtering, search, find-by-email with case-insensitive lookup
224 - - 12 new search_repo integration tests (`crates/db-sqlite/tests/search_repo_tests.rs`): FTS5 text search, type filter, priority filter, tag include/exclude, project search, multi-type results, pagination, special character escaping, edge cases
225 - - Test count: 259 -> 289 passed (10 ignored)
226 -
227 - ### Items resolved
228 - - `body_preview()` UTF-8 -- confirmed already uses `.chars().take(n)` with 18 tests
229 - - Dashboard stats batching -- confirmed already uses single query with 6 subqueries
230 - - search_repo testing gap -- 12 tests added (was B grade, now A-)
231 - - contact_repo testing gap -- 18 tests added (was B grade, now A-)
232 -
233 - ### Grades changed
234 - - **Testing**: A- (unchanged overall, but search_repo B->A- and contact_repo B->A- in module heatmap)
235 - - search_repo (Testing): B -> A- (12 integration tests covering FTS5 search, filters, pagination)
236 - - contact_repo (Testing): B -> A- (18 integration tests covering CRUD, sub-collections, filtering)
237 -
238 - ## Changes Since Last Audit (2026-02-28, third audit)
239 -
240 - ### Theme system rewrite
241 - - `src-tauri/src/commands/themes.rs`: Replaced hardcoded `~/Git/themes/` with multi-directory loading (bundled resources, dev fallback via `CARGO_MANIFEST_DIR`, user custom via `app_config_dir`). Added `AppHandle` params, `is_custom` field on `ThemeMeta`, `parse_meta()` helper, `find_theme_path()` with reverse-priority search. 6 unit tests for `validate_theme_id` and `parse_meta`.
242 - - `src-tauri/tauri.conf.json`: Added `"resources": ["../../../themes/*.toml"]` for bundled theme files.
243 - - `src-tauri/frontend/js/themes.js`: `getThemesByType()` returns `{ light, dark, highContrast }` with `high-contrast` variant routing.
244 - - `src-tauri/frontend/js/settings.js`: Added High Contrast `<optgroup>` in theme selector (conditionally rendered).
245 -
246 - ### New findings (minor)
247 - - **Missing `//!` docs on `smtp_client.rs`** (`src-tauri/src/email/smtp_client.rs`): Only non-trivial Rust source file without module-level docs. All other 141 files have them.
248 - - **`notify-debouncer-mini` not in workspace deps**: Defined directly in `src-tauri/Cargo.toml` as `"0.4"` instead of centralized in workspace `[workspace.dependencies]`. Minor inconsistency.
249 -
250 - ### No regressions
251 - - All tests pass, 0 clippy warnings
252 - - Theme system cleanly rewritten following BB's production-ready pattern
253 - - No grades changed (overall remains A-)
254 -
255 - ### Still open
256 - - ~~`list_completed_between` ignores date params (needs `completed_at` column migration)~~ -- RESOLVED (fourth audit): `completed_at` column added, `list_completed_between` now filters by date range
257 - - ~~MCP server tests (needs MCP test framework -- all 4 tests ignored)~~ -- RESOLVED: 20 integration tests added covering tasks, projects, events, utilities
258 - - Add `//!` docs to `smtp_client.rs`
259 - - Move `notify-debouncer-mini` to workspace deps
260 -
261 - ## Changes Since Last Audit (2026-02-28, third audit)
262 -
263 - ### Fifth-run full audit (2026-03-01)
264 -
265 - Fresh audit of entire codebase per audit.md. Test count: 338 (was 289). 0 clippy warnings. 0 `.unwrap()` in production code. 1 `unsafe` block (macOS dock icon via `objc2`, justified).
266 -
267 - ### Items resolved since third audit
268 -
269 - - **MCP server tests**: 20 integration tests added (`crates/goingson-mcp/tests/`) covering task CRUD, project CRUD, event CRUD, and utility tools. Cold spot grade: B -> A.
270 - - **`list_completed_between`**: `completed_at` column added via migration. Function now filters by date range. Logic bug resolved.
271 - - **Email command tests**: 8 tests (create, read/unread, snooze, waiting, archive, project link, unlinked, count)
272 - - **Contact command tests**: 5 tests (CRUD, email/phone, find by email)
273 - - **Event command tests**: 5 tests (CRUD, project link, upcoming, delete)
274 - - **goingson-mcp restructured**: lib+bin split enables test imports
275 -
276 - ### New findings
277 -
278 - 1. **`sync_service.rs` has zero tests (441 lines).** FK-ordered upsert/delete logic and trigger suppression are correctness-critical. The push/pull sync cycle is entirely untested. This is the highest-risk untested code in the project.
279 - - File: `src-tauri/src/sync_service.rs`
280 -
281 - 2. **IMAP client has zero tests (712 lines).** Complex OAuth/password branching, MIME parsing, folder operations, flag management -- all untested.
282 - - File: `src-tauri/src/email/imap_client.rs`
283 -
284 - 3. **OAuth token manager untested.** Token refresh flows and keychain storage for 4 providers (Google, Microsoft, Yahoo, Fastmail) have no tests.
285 - - File: `src-tauri/src/oauth/token_manager.rs`
286 -
287 - 4. **Plugin API bindings have 1 test for 538 lines.** `api.rs` exposes GoingsOn types to Rhai scripts with manual field-by-field mapping. A single test covers the entire surface. If a field is renamed in core, the Rhai binding silently returns wrong data.
288 - - File: `crates/plugin-runtime/src/api.rs`
289 -
290 - 5. **`sync_service.rs` uses `format!` errors instead of typed errors.** Functions return `Result<_, String>` with `format!("Failed to ...")` messages. Every other module uses typed `CoreError` or `ApiError`.
291 - - File: `src-tauri/src/sync_service.rs:59`
292 -
293 - 6. **CSS file is 6,551 lines with no section markers.** No table of contents, no `/* === Section === */` delimiters. Navigation requires text search.
294 - - File: `src-tauri/frontend/css/styles.css`
295 -
296 - 7. **`TaskSortColumn::sql_column()` in core crate returns raw SQL expressions.** The core crate should be storage-agnostic, but this method returns SQLite-specific SQL fragments including table aliases (`t.`, `p.`) and a `CASE` expression. Layer violation.
297 - - File: `crates/core/src/models/task.rs:407-414`
298 -
299 - ### Grades assessed (fresh audit perspective)
300 -
301 - | Dimension | Previous | Fresh Audit | Notes |
302 - |-----------|:--------:|:-----------:|-------|
303 - | Code Quality | A | A | Unchanged |
304 - | Architecture | A | A+ | Clean 5-crate workspace, pre-computed response pattern, repository traits |
305 - | Testing | A- | A- | 338 tests (up from 289); MCP/search/contact gaps closed; sync/IMAP/OAuth still untested |
306 - | Security | A | A | Unchanged |
307 - | Performance | A- | A | Dashboard stats confirmed as single query with 6 subqueries |
308 - | Documentation | A | A | 3,621 doc comments, module-level docs on every file |
309 - | Dependencies | A | A | All pinned at workspace level, core crate has zero framework deps |
310 - | Frontend | A- | A | 38 IIFE modules in GoingsOn namespace, centralized state, 200 escapeHtml calls |
311 -
312 - **Overall: A- -> A** (MCP test gap resolved, test count +49, list_completed_between fixed, dashboard stats confirmed efficient)
313 -
314 - ### New action items
315 -
316 - Filed in docs/todo/todo.md:
317 - - Add integration tests for `sync_service.rs` (highest priority -- FK-ordering and trigger suppression logic)
318 - - Add integration tests for IMAP client (mock MIME parsing and folder ops)
319 - - Convert sync service to typed errors (`Result<_, CoreError>` instead of `Result<_, String>`)
320 - - Expand plugin API tests (test each type exposed to Rhai)
321 - - Move `sql_column()` out of core (relocate to `db-sqlite/repository/task_repo.rs`)
322 - - Add section markers to `styles.css`
323 -
324 - ### No regressions
325 - - 338 tests pass (10 ignored -- macOS keychain + MCP framework tests)
326 - - cargo check + clippy clean
327 - - Zero `.unwrap()` in production code
328 - - 12 `.expect()` in production code (all justified)
329 - - 1 `unsafe` block (macOS dock icon, justified)
330 -
331 - ### Still open (8 items)
332 - - Add `//!` docs to `smtp_client.rs`
333 - - Move `notify-debouncer-mini` to workspace deps
334 - - Add sync_service.rs tests
335 - - Add IMAP client tests
336 - - Convert sync service to typed errors
337 - - Expand plugin API tests
338 - - Move `sql_column()` out of core
339 - - Add section markers to `styles.css`
340 -
341 - ## Changes Since Previous Audit (2026-03-01, fifth-run audit findings)
342 -
343 - ### Cleanup and resolution phase (2026-03-02)
344 -
345 - All actionable findings from the fifth-run audit resolved. Test count: 435 (was 338). 10 ignored. 0 clippy warnings. 0 failures.
346 -
347 - ### Items resolved
348 -
349 - - **Sync service typed errors**: Converted `sync_service.rs` from `Result<_, String>` with `format!()` messages to `Result<_, CoreError>`. All functions now use the project's typed error system.
350 - - File: `src-tauri/src/sync_service.rs`
351 -
352 - - **`sql_column()` moved out of core**: Relocated `TaskSortColumn::sql_column()` from `crates/core/src/models/task.rs` to `crates/db-sqlite/` where it belongs. Core crate no longer returns SQLite-specific SQL fragments.
353 -
354 - - **CSS section markers**: Added table of contents with 60 numbered sections to `src-tauri/frontend/css/styles.css`. Navigation no longer requires text search.
355 -
356 - - **SMTP client docs**: Added `//!` module-level documentation to `src-tauri/src/email/smtp_client.rs`. All source files now have module docs.
357 -
358 - - **Sync service tests**: 6 unit tests added for sync_service.rs (FK-ordered upsert/delete logic, trigger suppression).
359 - - File: `src-tauri/src/sync_service.rs`
360 -
361 - - **Plugin API tests**: 29 tests added for Rhai API bindings in plugin-runtime, covering field-by-field mapping for all exposed GoingsOn types.
362 - - File: `crates/plugin-runtime/src/api.rs`
363 -
364 - - **IMAP HTML helper tests**: 35 unit tests added for pure functions: `strip_html` (14 tests), `extract_href` (6 tests), `strip_tags_simple` (6 tests), `decode_html_entities` (9 tests). Covers normal HTML, edge cases (empty, malformed), entity decoding (named + numeric + typography), link extraction with URL dedup, script/style/head removal.
365 - - File: `src-tauri/src/email/imap_client.rs`
366 -
367 - ### Test count change
368 - - 338 → 435 passed (+97 tests: 35 IMAP helpers, 29 plugin API, 6 sync service, 27 from earlier resolution phases)
369 -
370 - ### Grades changed
371 - - Testing: A- (unchanged overall; sync/IMAP/plugin gaps now closed)
372 - - src-tauri/sync_service (Testing): B → A- (6 tests)
373 - - src-tauri/email/imap_client (Testing): untested → A- (35 tests on pure functions; MIME parsing and folder ops still untested)
374 - - crates/plugin-runtime/api (Testing): C → A (29 tests covering all exposed types)
375 -
376 - ### Still open (3 items)
377 - - Move `notify-debouncer-mini` to workspace deps (minor inconsistency)
378 - - OAuth token manager tests (token refresh flows, keychain storage)
379 - - Full IMAP client integration tests (mock MIME parsing and folder ops — pure function helpers now tested)
380 -
381 - ## Changes Since Previous Audit (2026-03-02, type safety newtypes)
382 -
383 - ### Entity ID newtypes + stringly-typed field fixes
384 -
385 - 11 entity ID newtypes introduced via `define_uuid_id!` macro in `crates/core/src/id_types.rs` with feature-gated sqlx impls (`#[cfg(feature = "sqlx-sqlite")]`). All model structs updated. ViewFilters and SavedView stringly-typed fields replaced with proper enums. `SortDirection::sql()` moved from core to db-sqlite (layer leak fixed).
386 -
387 - ### Types defined
388 - - `define_uuid_id!` macro: TaskId, ProjectId, EventId, EmailId, ContactId, MilestoneId, WeeklyReviewId, SavedViewId, EmailAccountId, LlmSettingsId, UserId
389 - - `SortField` enum: replaces `SavedView.sort_by: Option<String>`
390 - - `ViewFilters.status`: `Option<Vec<String>>` → `Option<Vec<TaskStatus>>`
391 - - `ViewFilters.priority`: `Option<Vec<String>>` → `Option<Vec<Priority>>`
392 - - `SavedView.sort_order`: `String` → `SortDirection` (existing enum)
393 -
394 - ### Grades changed
395 - - Type Safety: A- → A (11 entity ID newtypes, stringly-typed fields eliminated, layer leak fixed)
@@ -1,635 +0,0 @@
1 - # GoingsOn -- Competitive Analysis
2 -
3 - Last updated: 2026-02-27
4 -
5 - ## Positioning
6 -
7 - GoingsOn is the only app that combines tasks, email, calendar, contacts, and weekly review in a single offline-first native application. Every competitor covers 1-2 of these domains. Built with Tauri 2 (Rust backend, vanilla JS frontend, SQLite), it targets independent workers who want a fast, offline-capable workspace without SaaS lock-in.
8 -
9 - The core advantage is integration depth (tasks + email + calendar + contacts + projects in one local-first app) combined with privacy (no server, no tracking, zero-knowledge sync) and extensibility (Rhai plugins, MCP server). No single competitor matches this combination. In a market where annual subscription costs range from $48 to $408, GoingsOn ships free and source-available.
10 -
11 - ## Pricing Comparison
12 -
13 - | App | Price | Model |
14 - |-----|-------|-------|
15 - | **GoingsOn** | **Free** | Source-available, no subscription |
16 - | TickTick Premium | ~$28-36/yr | Subscription |
17 - | Fantastical | ~$40/yr | Subscription |
18 - | Todoist Pro | $48/yr | Cloud subscription |
19 - | Obsidian Sync | $48/yr | Optional sync add-on |
20 - | Spark Premium | $60/yr | Subscription |
21 - | Things 3 | ~$80 | One-time purchase (Apple only) |
22 - | Notion Plus | $120/yr | Cloud subscription |
23 - | Sunsama | $192/yr | Cloud subscription |
24 - | Akiflow | $228/yr | Cloud subscription |
25 -
26 - ## Feature Matrix
27 -
28 - | Feature | GO | Todoist | Things 3 | TickTick | Notion | Obsidian | Fantastical | Spark | Sunsama | Akiflow |
29 - |---------|:--:|:------:|:--------:|:--------:|:------:|:--------:|:-----------:|:-----:|:-------:|:-------:|
30 - | **Tasks** | Yes | Yes | Yes | Yes | Yes | Plugin | Basic | No | Yes | Yes |
31 - | **Email client** | Yes | No | No | No | No | No | No | Yes | Partial | Partial |
32 - | **Calendar** | Yes | Partial | Read-only | Yes | Basic | Plugin | Yes | No | Yes | Yes |
33 - | **Contacts** | Yes | No | No | No | No | No | No | No | No | No |
34 - | **Weekly review** | Yes | No | No | No | No | No | No | No | Yes | No |
35 - | **Offline-first** | Yes | No | Yes | No | Limited | Yes | Yes | No | No | No |
36 - | **Local data** | Yes | No | Partial | No | No | Yes | Partial | No | No | No |
37 - | **Source-available** | Yes | No | No | No | No | No | No | No | No | No |
38 - | **Plugin system** | Yes | No | No | No | API | Yes | No | No | No | No |
39 - | **MCP/LLM tools** | Yes | No | No | No | AI built-in | Plugin | No | AI built-in | No | AI built-in |
40 - | **Urgency scoring** | Yes | 4 levels | No | No | No | No | No | No | No | No |
41 - | **Cross-platform** | Yes | Yes | Apple only | Yes | Yes | Yes | Apple+Win | Yes | Yes | Mac/Win |
42 - | **Linux** | Yes | Yes | No | Yes | Yes | Yes | No | No | No | No |
43 - | **Mobile** | In progress | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Web only |
44 - | **AI features** | LLM templates | Yes | No | No | Yes | No | No | Yes | Planned | Yes |
45 - | **Team collab** | No | Yes | No | No | Yes | No | No | Yes | Yes | No |
46 - | **Free tier** | Yes | Yes | No | Yes | Yes | Yes | Limited | Yes | No | No |
47 -
48 - ## Competitor Deep Dives
49 -
50 - ### 1. Todoist
51 -
52 - **Task management SaaS with natural language input, collaboration, and AI assistant.**
53 -
54 - Pricing: Free (5 projects) | Pro $4/mo ($48/yr) | Business $6-10/user/mo
55 -
56 - | Feature GoingsOn Lacks | Notes |
57 - |------------------------|-------|
58 - | Kanban board view | Drag tasks between columns; useful for visual workflows |
59 - | Location-based reminders | Triggers reminders when arriving/leaving a location `[DATA-HUNGRY]` |
60 - | Karma gamification (points, streaks, levels) | Productivity scoring from Beginner to Enlightenment `[BLOAT]` |
61 - | Real-time collaboration (shared projects, task comments, assignments) | Multi-user shared projects with task delegation |
62 - | AI voice-to-task (Todoist Ramble) | Speak naturally, AI extracts tasks/dates/details `[DATA-HUNGRY]` |
63 - | AI task suggestions & auto-breakdown | AI suggests subtasks, rewrites vague tasks, recommends next steps `[DATA-HUNGRY]` |
64 - | Activity log / audit history | Full history of all changes across projects |
65 - | Vacation mode (protects streaks) | Pauses gamification during time off `[BLOAT]` |
66 - | Web app | Full browser-based access with no install |
67 - | Template gallery | Pre-built project templates for common workflows |
68 - | Urgent reminders (iOS full-screen alarm) | Persistent alarm that overrides Do Not Disturb `[INVASIVE]` |
69 -
70 - ### 2. Things 3
71 -
72 - **Elegant GTD-style task manager for Apple platforms. One-time purchase.**
73 -
74 - Pricing: Mac $49.99 | iPhone $9.99 | iPad $19.99 (one-time, no subscription)
75 -
76 - | Feature GoingsOn Lacks | Notes |
77 - |------------------------|-------|
78 - | Areas (high-level life categories) | Group projects under areas like "Work", "Personal", "Health" |
79 - | Today / This Evening split | Separate morning and evening task sections within the day |
80 - | Logbook (completed task archive) | Browsable archive of everything you've finished |
81 - | Someday list | GTD "Someday/Maybe" list for non-urgent ideas |
82 - | Quick Entry with project/heading targeting | System-wide shortcut to add tasks directly into a specific heading |
83 - | Headings within projects | Visual section dividers within a project task list |
84 - | Apple Watch app | Task management from the wrist |
85 - | Apple Shortcuts / URL scheme | Deep automation via Shortcuts and x-callback-url |
86 - | Magic Plus button (context-aware) | "+" button behavior changes based on current view |
87 - | Mail-to-Things (forward emails as tasks) | Forward an email to a special address to create a task |
88 - | Type-to-filter in any list | Start typing to instantly filter the visible list |
89 -
90 - ### 3. TickTick
91 -
92 - **All-in-one task manager with calendar, habits, Pomodoro timer, and Kanban.**
93 -
94 - Pricing: Free (9 lists, 99 tasks/list) | Premium ~$28-36/yr ($2.80/mo)
95 -
96 - | Feature GoingsOn Lacks | Notes |
97 - |------------------------|-------|
98 - | Habit tracker | Daily habit tracking with streaks and completion charts |
99 - | Pomodoro / focus timer | Built-in countdown and stopwatch with session tracking |
100 - | White noise / ambient sounds | Background sounds (rain, cafe, nature) during focus sessions `[BLOAT]` |
101 - | Eisenhower Matrix view | Four-quadrant urgent/important grid view |
102 - | Kanban board view | Column-based task organization |
103 - | Multiple calendar views (5 types) | Day, 3-day, week, month, and agenda calendar views |
104 - | Calendar subscription (read external) | Subscribe to external calendars (read-only) |
105 - | Task duration estimates with stats | Estimate time per task, track actual vs estimated |
106 - | Achievement badges | Gamification rewards for productivity milestones `[BLOAT]` |
107 - | Smart date parsing in descriptions | Detects dates within task descriptions |
108 - | Web app | Browser-based access |
109 - | Apple Watch / Wear OS | Wrist-based task management |
110 - | Widgets (iOS/Android/desktop) | Home screen widgets showing tasks and calendar |
111 -
112 - ### 4. Notion
113 -
114 - **Workspace combining docs, databases, wikis, project management, and AI agents.**
115 -
116 - Pricing: Free (personal) | Plus $8-10/mo | Business $20/user/mo | Enterprise custom
117 -
118 - | Feature GoingsOn Lacks | Notes |
119 - |------------------------|-------|
120 - | Freeform documents / pages | Rich-text documents with nested blocks and embeds |
121 - | Relational databases (20+ property types) | Structured data with relations, rollups, formulas, filters |
122 - | 6 database views (table, board, timeline, calendar, gallery, list) | Multiple visual layouts for the same data |
123 - | Wiki / knowledge base | Team wiki with verified pages and breadcrumb navigation |
124 - | AI agents (autonomous multi-step workflows) | Agent performs 20min of autonomous work across hundreds of pages `[DATA-HUNGRY]` |
125 - | Automations (trigger-based workflows) | If-then automations on database changes |
126 - | Forms builder | Create forms that feed into databases |
127 - | Sites (publish pages as website) | Turn Notion pages into public websites |
128 - | Template gallery (thousands) | Massive community template ecosystem |
129 - | Real-time collaboration (multiplayer editing) | Multiple users edit the same page simultaneously |
130 - | Web app | Full browser-based access |
131 - | API (public REST) | Third-party integrations via public API |
132 - | Embeds (50+ services) | Embed Figma, Loom, Google Maps, Miro, etc. inline |
133 - | Comments and mentions | @mention users, threaded comments on blocks |
134 - | Conditional database coloring | Color rows/cells based on formula conditions |
135 - | People directory | Auto-built team directory from workspace members `[BLOAT]` |
136 - | Version history (7-90 days by plan) | Page-level version history with restore |
137 -
138 - ### 5. Spark Mail
139 -
140 - **Smart email client with AI writing, inbox triage, and team collaboration.**
141 -
142 - Pricing: Free (basic) | Premium $4.99/mo ($59.99/yr) | Team $6.99/user/mo
143 -
144 - | Feature GoingsOn Lacks | Notes |
145 - |------------------------|-------|
146 - | Smart Inbox (auto-categorization) | Auto-sorts into Personal, Notifications, Newsletters `[DATA-HUNGRY]` |
147 - | Gatekeeper (unknown sender blocking) | Holds first-time senders in a queue for approval |
148 - | AI email compose / rephrase / translate | AI drafts, rewrites, and translates emails `[DATA-HUNGRY]` |
149 - | AI email summarization | Summarize long threads in one click `[DATA-HUNGRY]` |
150 - | My Writing Style (AI learns your tone) | AI mimics your personal writing style `[DATA-HUNGRY]` |
151 - | Send Later (scheduled send) | Compose now, send at a specified time |
152 - | Email signatures (rich, multiple) | Multiple formatted signatures, auto-switch per account |
153 - | Shared team inbox | Multiple team members manage one inbox |
154 - | Shared drafts (real-time co-editing) | Collaborate on email drafts in real time |
155 - | Email assignment to team members | Assign emails to colleagues with status tracking |
156 - | Smart notifications (only for important) | Only notifies for emails from real people, not newsletters `[DATA-HUNGRY]` |
157 - | Priority sender badges | Visual indicator for VIP contacts |
158 - | Newsletter digest | Groups newsletters into a single digest |
159 - | Quick replies (one-tap responses) | Suggested short responses at the bottom of emails |
160 - | Follow-up reminders | Automatic reminders when sent emails get no reply |
161 - | Email pin | Pin important emails to the top of inbox |
162 -
163 - ### 6. Fantastical
164 -
165 - **Premium calendar app with natural language, scheduling proposals, and weather.**
166 -
167 - Pricing: Free (limited) | Premium ~$3.33/mo ($40/yr) | Team pricing available
168 -
169 - | Feature GoingsOn Lacks | Notes |
170 - |------------------------|-------|
171 - | Calendar sets (grouped calendar toggling) | Switch between "Work" and "Home" calendar groups with one click |
172 - | Location-based calendar sets | Auto-switch calendar set when arriving/leaving a location `[DATA-HUNGRY]` |
173 - | Weather forecast in calendar | 10-day AccuWeather forecast inline in calendar views `[BLOAT]` |
174 - | Meeting/scheduling proposals | Send event proposals with multiple time options to invitees |
175 - | Conference call auto-detection | Auto-detects Zoom/Teams/Meet links and adds join buttons |
176 - | Conference call auto-creation | Automatically adds video call links to scheduled events |
177 - | Travel time estimates | Shows travel time between events on the calendar |
178 - | Interesting calendars (sports, TV, holidays) | Subscribe to curated event calendars `[BLOAT]` |
179 - | Apple Watch app | Calendar on the wrist |
180 - | Apple Vision Pro support | Spatial calendar in visionOS |
181 - | Multiple calendar views (day/week/month/quarter/year) | More granular view options including quarter and year |
182 - | Availability sharing (Openings) | Share available time slots via link for others to book |
183 - | Task integration (Apple Reminders, Todoist) | Show tasks from external task apps on the calendar |
184 - | Forward-to-Fantastical (email to event) | Forward an email to create a calendar event automatically |
185 - | Date & time proposals in templates | Scheduling templates with reusable configurations |
186 -
187 - ### 7. Obsidian
188 -
189 - **Local-first markdown knowledge base with graph view and 2700+ community plugins.**
190 -
191 - Pricing: Free (core app) | Sync $4/mo | Publish $8/site/mo | Catalyst $25 one-time
192 -
193 - | Feature GoingsOn Lacks | Notes |
194 - |------------------------|-------|
195 - | Freeform markdown documents | Long-form writing with wiki-style linking |
196 - | Graph view (knowledge visualization) | Interactive visual map of note connections |
197 - | Backlinks (bidirectional linking) | Every note shows what links to it automatically |
198 - | Canvas (infinite whiteboard) | Spatial arrangement of notes, images, and links |
199 - | Daily notes | Auto-created daily journal entries |
200 - | 2700+ community plugins | Massive extensibility ecosystem |
201 - | Dataview (database queries on markdown) | SQL-like queries across your notes |
202 - | Templater (advanced templates) | Dynamic templates with JavaScript logic |
203 - | Local markdown files (open format) | Plain .md files in a folder, readable by any editor |
204 - | Publish (notes as website) | Publish notes as a static website |
205 - | Excalidraw (drawing/diagramming) | Built-in whiteboard via plugin |
206 - | Vim mode | Native Vim keybindings in the editor |
207 - | Community themes (hundreds) | Massive theme ecosystem |
208 - | CLI tool | Command-line access to vaults (new in 2026) |
209 -
210 - ### 8. Sunsama
211 -
212 - **Guided daily planner that pulls tasks from external tools into a timeboxed schedule.**
213 -
214 - Pricing: $16/mo annually ($192/yr) | $20/mo monthly | No free plan
215 -
216 - | Feature GoingsOn Lacks | Notes |
217 - |------------------------|-------|
218 - | Guided daily planning ritual (multi-step) | Step-by-step morning workflow: review yesterday, pick today's tasks, timebox |
219 - | Daily shutdown ritual | Guided end-of-day reflection and task rollover |
220 - | Focus mode / focus timer | Single-task focus mode with timer (Pomodoro optional) |
221 - | Task time estimates with actuals tracking | Estimate task duration, compare to actual time spent |
222 - | Automatic timeboxing (planned) | AI auto-schedules tasks based on priority and availability `[DATA-HUNGRY]` |
223 - | Pull tasks from external tools | Import from Asana, Trello, Jira, ClickUp, Notion, Linear, GitHub |
224 - | Pull tasks from Slack/Teams messages | Turn chat messages into tasks `[DATA-HUNGRY]` |
225 - | Gmail/Outlook email-to-task (native) | Pull emails as tasks from integrated email accounts |
226 - | Daily Slack/Teams standup post | Auto-post your daily plan to a team channel |
227 - | Workload guardrails | Warns when you've scheduled more than your target hours |
228 - | Drag tasks between days | Move unfinished tasks to future days visually |
229 - | Weekly analytics | Time spent per day/week with trend visualization |
230 - | Theme days | Label days with a focus theme (e.g., "Deep Work Monday") |
231 - | Unified task view across integrations | Single list pulling from 10+ external tools |
232 -
233 - ## Common Missing Features
234 -
235 - Features that appear in 3+ competitors and GoingsOn currently lacks.
236 -
237 - ### Worth Adding
238 -
239 - | Feature | Competitors | Reasoning |
240 - |---------|-------------|-----------|
241 - | **Kanban / board view** | Todoist, TickTick, Notion, Obsidian (plugin) | Visual alternative to list/table views. Low complexity, high utility for project-oriented users. Could be a project-level view option. |
242 - | **Apple Watch app** | Things 3, TickTick, Fantastical | Quick task capture and glanceable agenda from the wrist. Tauri 2 doesn't target watchOS natively, so this would be a separate Swift micro-app. Post-launch. |
243 - | **Task time estimates with tracking** | TickTick, Sunsama, Todoist (duration) | GoingsOn already has time blocking with duration. Adding estimate-vs-actual tracking is a small extension with real value for freelancers billing by the hour. |
244 - | **Widgets (iOS/Android/desktop)** | TickTick, Things 3, Fantastical, Notion | Home screen widgets for today's tasks and upcoming events. Requires native widget code per platform. Post-mobile-launch. |
245 - | **Template gallery / starter templates** | Todoist, Notion, TickTick | Ship a small set of built-in project templates (e.g., "Freelance Project", "Content Pipeline", "Job Search"). Not a marketplace, just bundled starter configs. |
246 - | **External calendar sync (Google/Apple)** | Fantastical, TickTick, Sunsama, Notion | Already planned. Google Calendar, Apple Calendar, and CalDAV sync are on the roadmap. High priority for launch. |
247 - | **Send Later (scheduled email send)** | Spark, (Todoist has scheduled tasks) | Simple to implement -- queue outbound email with a send-at timestamp. Useful for timezone-aware communication. |
248 - | **Multiple calendar views (month/quarter/year)** | Fantastical, TickTick, Notion | GoingsOn has a day plan timeline. A month view is the most obvious gap. Quarter/year views are lower priority. |
249 -
250 - ### Consider
251 -
252 - | Feature | Competitors | Reasoning |
253 - |---------|-------------|-----------|
254 - | **Focus / Pomodoro timer** | TickTick, Sunsama, Obsidian (plugin) | Natural fit alongside time blocking and day plan. Keep it simple: a countdown timer linked to a task, no gamification. |
255 - | **Activity log / history** | Todoist, Notion (version history), Spark | An audit trail of task/project changes. Useful for accountability and undo. Could be implemented locally in SQLite with minimal overhead. |
256 - | **Guided daily planning ritual** | Sunsama, (Things 3 Today/Evening), TickTick | GoingsOn has weekly review but no daily planning ritual. A lightweight morning workflow (review overdue, pick today's tasks, timebox) would complement the existing weekly review without adding bloat. |
257 -
258 - ### Skip
259 -
260 - | Feature | Competitors | Reasoning |
261 - |---------|-------------|-----------|
262 - | **Web app** | Todoist, TickTick, Notion, Sunsama | Contradicts local-first architecture and privacy model. Tauri desktop + mobile covers the target audience. A web app would require a server and undermine the offline-first value prop. |
263 - | **Real-time collaboration** | Todoist, Notion, Spark, Sunsama | GoingsOn targets independent workers, not teams. Collaboration adds massive complexity (CRDT/OT, permissions, presence) with minimal value for the target user. |
264 - | **Freeform documents / notes** | Notion, Obsidian, (Things 3 has task notes) | GoingsOn has task annotations and descriptions. Full document editing is a massive scope expansion that competes with dedicated tools. Better to integrate with Obsidian via plugin than to rebuild a notes system. |
265 - | **AI writing / compose assistance** | Todoist, Spark, Notion | GoingsOn already has LLM templates and AI-Fill. Adding full AI compose for emails would require sending email content to a third party, conflicting with the privacy-first model. The existing local LLM approach (Ollama) is the right path. |
266 -
267 - ## What We Offer That Competitors Don't
268 -
269 - - **Only app with all 5 domains** -- tasks + email + calendar + contacts + weekly review in one native app. No other tool does this. Deep cross-linking between all entity types (email-to-task, task-to-event, contact-to-everything).
270 - - **Offline-first with zero cloud dependency** -- SQLite local storage, no account creation, no data on third-party servers. Works fully offline.
271 - - **Zero-knowledge sync** -- when cloud sync ships, all data is end-to-end encrypted client-side (XChaCha20-Poly1305) before leaving the device. The server literally cannot read your data.
272 - - **Pluggable sync providers** -- not locked into one cloud. Choose from GoingsOn Cloud, WebDAV, Dropbox, Google Drive, S3, OneDrive, or a local folder.
273 - - **Source-available** -- unique among all competitors.
274 - - **TaskWarrior-style urgency scoring** -- algorithmic priority considering due date proximity, age, priority level, overdue penalty, started bonus, tag bonuses. More sophisticated than any competitor's priority system.
275 - - **Rhai plugin system** -- user-extensible without forking. Obsidian has plugins but they're JavaScript with no sandboxing.
276 - - **MCP server for Claude integration** -- 40+ structured tools across all domains. No other productivity app exposes this level of programmatic access to an LLM agent. App auto-refreshes when agents modify data.
277 - - **LLM template system** -- dynamic and static LLM templates embedded in text fields, with AI-Fill button for on-demand generation.
278 - - **OAuth2 email for 4 providers** -- Fastmail (JMAP), Google, Microsoft, Yahoo with PKCE flow. Most email clients support fewer OAuth providers.
279 - - **Natural-language quick-add** -- type `Fix bug +urgent project:GoingsOn pri:H due:tomorrow recur:weekly` instead of filling out a form.
280 - - **10 built-in themes** -- Neobrute, Catppuccin (Latte/Frappe/Macchiato/Mocha), Dracula, Nord, Tokyo Night, Flatwhite, Ayu Light.
281 - - **Skeubrute design system** -- distinctive visual identity: paper texture, embossed text, debossed inputs, tactile buttons.
282 - - **Single codebase for desktop + mobile** -- same Rust backend and JS frontend for macOS, Windows, Linux, iOS, and Android via Tauri 2. No Electron, no web framework overhead.
283 - - **Keyboard-first UX** -- vim-style navigation (g+t, j/k, q for quick-add), global shortcuts overlay, full keyboard accessibility.
284 - - **No subscription** -- in a market where annual costs range from $48 to $408.
285 -
286 - ## Key Dynamics
287 -
288 - - Every major competitor is shipping AI features. GoingsOn's MCP server is a developer-facing answer, but end-user AI (summarize, compose, schedule) is the biggest gap.
289 - - Sunsama ($192/yr) is the closest philosophical match -- daily planning, weekly review, calendar+tasks+email integration -- but cloud-only and expensive.
290 - - The offline-first, local-data, source-available combination is a genuine moat for privacy-conscious users.
291 - - Unified workspace eliminates app-switching (vs. using Todoist + Spark + Fantastical separately).
292 - - No subscription required for core features (vs. Fantastical/Sunsama/Spark subscriptions).
293 -
294 - **Biggest competitive gaps to close:**
295 - 1. Kanban/board view for tasks (table stakes for task apps)
296 - 2. Monthly calendar view (everyone expects it)
297 - 3. External calendar sync (already planned, high priority)
298 - 4. Focus timer alongside time blocking (natural fit)
299 - 5. Guided daily planning ritual (complements weekly review)
300 -
301 - **Strongest defenses:**
302 - - Unified workspace eliminates app-switching
303 - - Local-first with E2E encrypted sync
304 - - No subscription for core features
305 - - MCP server for AI agents (unique)
306 - - Plugin system for user extensibility (only Obsidian competes here)
307 -
308 - ## Target Users
309 -
310 - - Independent workers, freelancers, solopreneurs, and solo creators who juggle multiple projects across different domains (software, writing, art, business)
311 - - Power users who want keyboard-driven workflows, natural-language input, and scriptable automation
312 - - Privacy-conscious professionals who want local data ownership with optional end-to-end encrypted sync
313 - - People frustrated with using 4-5 separate apps (task manager + email client + calendar + contacts + notes) who want a single integrated tool
314 -
315 - ## Full Feature Inventory
316 -
317 - Appendix: complete feature table from GoingsOn with current status.
318 -
319 - ### Task Management
320 -
321 - | Feature | Status | Use Case |
322 - |---------|--------|----------|
323 - | Full CRUD with statuses (Pending, Started, Completed, Deleted) | Done | Track work items through their lifecycle |
324 - | Priority levels (High, Medium, Low) | Done | Triage and focus on what matters most |
325 - | Tags system | Done | Flexible cross-cutting categorization (e.g., "quick-win", "blocked", "research") |
326 - | Recurrence (Daily, Weekly, Monthly) | Done | Repeating tasks auto-create next instance on completion |
327 - | Subtasks (ordered checkbox items) | Done | Break large tasks into steps without creating separate tasks |
328 - | Annotations (timestamped notes) | Done | Add context, updates, or comments over time without editing the task description |
329 - | Smart urgency calculation algorithm | Done | Auto-prioritize based on due date proximity, priority, age, status, and tags |
330 - | Quick-add parser with natural language | Done | Type `Fix bug +urgent project:GoingsOn pri:H due:tomorrow recur:weekly` instead of filling out a form |
331 - | Snooze/defer tasks until a date | Done | Hide tasks from active view until they become relevant |
332 - | Waiting-for-response tracking | Done | Track tasks/emails blocked on someone else, with expected response dates and overdue reminders |
333 - | Email-to-task conversion | Done | One-click conversion of an email into a task, preserving the link back to the source email |
334 - | Task-event auto-linking | Done | Tasks with due dates automatically create linked calendar events |
335 - | Task focus system (weekly priorities) | Done | Star tasks for the week during weekly review, filter by focus |
336 - | Milestone assignment | Done | Group tasks under project milestones with progress tracking |
337 - | Bulk actions (archive, delete, snooze) | Done | Multi-select with checkboxes, shift-click range select, bulk operations |
338 - | Column sorting on task tables | Done | Sort by urgency, priority, due date, project, etc. |
339 - | Virtual scrolling for large lists | Done | Handle hundreds of tasks without UI lag |
340 - | Time blocking (scheduled start + duration) | Done | Assign time blocks to tasks, see them on the day plan timeline |
341 -
342 - ### Project Management
343 -
344 - | Feature | Status | Use Case |
345 - |---------|--------|----------|
346 - | Full CRUD with 7 project types | Done | Organize work into typed projects (Job, SideProject, Company, Essay, Article, Painting, Other) |
347 - | Project status tracking (Active, OnHold, Completed, Archived) | Done | Lifecycle management for projects |
348 - | Project dashboard with linked tasks/events/emails | Done | Single view of everything related to a project |
349 - | Project milestones with progress bars | Done | Break projects into phases, track completion percentage |
350 - | Milestone auto-completion | Done | Milestone auto-marks as complete when all its tasks are done |
351 - | Milestone reordering | Done | Arrange milestones in execution order with up/down controls |
352 - | Filter tasks by milestone | Done | Focus on tasks within a specific milestone |
353 -
354 - ### Email Integration
355 -
356 - | Feature | Status | Use Case |
357 - |---------|--------|----------|
358 - | IMAP client for fetching emails | Done | Pull emails from any IMAP provider into the app |
359 - | SMTP client for sending/composing | Done | Send emails without leaving the app |
360 - | OAuth2 authentication (Fastmail, Google, Microsoft, Yahoo) | Done | Secure, modern auth without storing raw passwords |
361 - | JMAP protocol support (Fastmail) | Done | Faster, more efficient sync than IMAP for supported providers |
362 - | PKCE flow for desktop OAuth | Done | Secure OAuth for native desktop apps |
363 - | Email threading (conversation view) | Done | Group related emails by thread, see conversation history |
364 - | Archive system | Done | Archive emails to clean the inbox without deleting |
365 - | Read/unread status sync from IMAP server | Done | Accurate read status matching the mail server |
366 - | Email-to-project linking | Done | Associate emails with projects for context |
367 - | Email-to-task conversion with source tracking | Done | Turn actionable emails into tasks with one click |
368 - | Email-to-event conversion | Done | Create calendar events from email content |
369 - | Snooze emails until a date | Done | Temporarily hide emails, get notified when they resurface |
370 - | Auto-suggest contact from email sender | Done | Automatically matches emails to existing contacts |
371 - | Create contact from email address | Done | One-click contact creation from email sender info |
372 - | Reader mode (HTML-to-clean-text) | Done | Strip formatting cruft for readable display; never renders raw HTML (security) |
373 - | Open original HTML in browser | Done | View the original formatted email when needed |
374 - | Email auto-sync (configurable intervals) | Done | Background sync at 5/15/30/60 min intervals per account |
375 - | Unread count on dashboard | Done | At-a-glance inbox status |
376 - | Secure credential storage (OS keychain) | Done | Email passwords and OAuth tokens stored in macOS Keychain / Windows Credential Manager / Linux Secret Service |
377 - | Multiple account support | Done | Manage several email accounts in one inbox |
378 -
379 - ### Calendar & Events
380 -
381 - | Feature | Status | Use Case |
382 - |---------|--------|----------|
383 - | Full CRUD for calendar events | Done | Create, edit, delete events with title, description, location, time range |
384 - | Event recurrence (Daily, Weekly, Monthly) | Done | Repeating events auto-create next instance |
385 - | Task-event linking | Done | Completing a task updates/removes its linked event |
386 - | Date badge proximity coloring | Done | Color-coded event badges (today=green, tomorrow=yellow, this week=cyan, future=blue, past=gray) |
387 - | Event status indicator (green/yellow/red dot) | Done | Glanceable status: green=clear, yellow=event soon, red=event now |
388 - | Configurable lead time for reminders | Done | Set how far in advance you want to be notified (5/10/15/30/60 min) |
389 - | Time block types (Free, Personal, Vacation, Focus) | Done | Block time on your day plan with distinct visual styling |
390 - | Day Plan timeline view | Done | Visual day timeline showing events, time blocks, and task blocks with conflict detection |
391 - | Paint-to-create on timeline | Done | Click/drag on the timeline to create events, time blocks, or link tasks |
392 - | Day Plan swipe navigation (mobile) | Done | Swipe left/right to move between days |
393 - | Vacation day toggles | Done | Mark days off in weekly review, shown as "Day Off" banner in day plan |
394 - | ICS calendar export | Done | Export events to standard calendar format |
395 - | Google Calendar sync | Planned | Two-way sync with Google Calendar via API |
396 - | Apple Calendar / iCloud CalDAV sync | Planned | Two-way sync with Apple Calendar |
397 - | CalDAV generic sync (Fastmail, Nextcloud) | Planned | Two-way sync with any CalDAV server |
398 - | Import .ics files | Planned | Import events from other calendar apps |
399 -
400 - ### Contact Management
401 -
402 - | Feature | Status | Use Case |
403 - |---------|--------|----------|
404 - | Full CRUD with sub-collections (emails, phones, social handles) | Done | Store rich contact records with multiple emails, phones, and social profiles |
405 - | Custom fields (label + value + optional URL) | Done | Add arbitrary metadata to contacts (e.g., "GitHub: user123" with link) |
406 - | Full-text search across contacts | Done | Search by name, nickname, company, notes, tags |
407 - | Tags for contact categorization | Done | Categorize contacts (e.g., "client", "vendor", "friend") |
408 - | Initials avatar | Done | Visual contact identification with colored circles |
409 - | Contact-task linking | Done | Associate tasks with a contact (e.g., "Follow up with Jane") |
410 - | Contact-event linking | Done | Associate events with a contact |
411 - | Auto-link contact when converting email to task/event | Done | Contact from email sender automatically linked |
412 - | Contact card in email thread view | Done | See who you're emailing with quick access to their contact record |
413 - | vCard (.vcf) import/export | Planned | Standard contact interchange format |
414 - | Apple Contacts import | Planned | Migrate from macOS Contacts app |
415 - | Google Contacts sync | Planned | Two-way sync with Google Contacts |
416 - | Microsoft Graph contacts sync | Planned | Two-way sync with Outlook contacts |
417 - | CardDAV sync (Fastmail, iCloud, Nextcloud) | Planned | Two-way sync with any CardDAV server |
418 - | LinkedIn connections import | Planned | Import professional network contacts |
419 -
420 - ### Day Planning & Weekly Review
421 -
422 - | Feature | Status | Use Case |
423 - |---------|--------|----------|
424 - | Day Plan timeline view with time blocks | Done | Visual daily schedule showing events, blocks, and task time |
425 - | Paint-to-create modal (Event / Time Block / Link Task) | Done | Quickly schedule by clicking on the timeline |
426 - | Conflict detection | Done | Warns when scheduling overlaps |
427 - | Collapsible unscheduled tasks sidebar | Done | See unassigned tasks alongside the timeline |
428 - | Weekly review tab with guided workflow | Done | Structured weekly reflection: past week stats, coming week preview, focus setting |
429 - | Past week statistics (completed tasks, events, overdue, pending) | Done | Understand what got done and what didn't |
430 - | Coming week preview (upcoming events, tasks due, overdue) | Done | Prepare for the week ahead |
431 - | Task focus system with weekly priorities | Done | Star tasks for the week, clear all, track focused projects |
432 - | Week timeline with activity dots | Done | Visual overview of each day's completions, events, and overdue items |
433 - | Carried-over tasks tracking | Done | See which tasks rolled over from prior weeks |
434 - | Project health indicators | Done | Per-project health status in the review |
435 - | Vacation day toggles (MTWTFSS) | Done | Mark days off, reflected in timeline and day plan |
436 - | Review completion tracking with notes | Done | Write review notes, track when review was completed |
437 - | Monday nudge (tab badge + startup toast) | Done | Reminder to do the weekly review at the start of each week |
438 - | Print styles for weekly review | Done | Print or save the review as a physical document |
439 - | Share as image export | Done | Export review as shareable image |
440 - | Auto-save draft on input change | Done | Never lose review notes mid-writing |
441 -
442 - ### Search & Navigation
443 -
444 - | Feature | Status | Use Case |
445 - |---------|--------|----------|
446 - | Full-text search (SQLite FTS5) across tasks, projects, emails, events, contacts | Done | Find anything instantly |
447 - | Search by type, project, date range | Done | Narrow results with filters (after:/before:/from:/to: syntax) |
448 - | Filters by project, tag, date range, status | Done | Filter any list view by multiple criteria (AND logic) |
449 - | Saved views (named filter combinations) | Done | Save frequently-used filter sets for one-click access |
450 - | Pinned views in sidebar | Done | Quick access to your most-used views |
451 - | URL routing (History API) | Done | Browser-style back/forward navigation between views |
452 - | Keyboard shortcuts with overlay (press ?) | Done | Vim-style navigation: g+t/e/p for views, j/k for list movement, a/c/n for actions |
453 - | Quick-add shortcut (q) | Done | Open quick-add from anywhere with one keypress |
454 - | Native menu bar with shortcuts | Done | macOS menu bar with Cmd+1-5 for views, Cmd+N for new task, Cmd+, for settings |
455 - | Context menus (right-click) | Done | Right-click tasks, emails, events, projects for quick actions |
456 - | Dynamic window title | Done | Window title reflects current view |
457 - | Virtual scrolling | Done | Smooth scrolling for lists with hundreds/thousands of items |
458 -
459 - ### Plugin System (Rhai Scripting)
460 -
461 - | Feature | Status | Use Case |
462 - |---------|--------|----------|
463 - | Rhai scripting engine with safety limits | Done | Run sandboxed scripts to extend the app |
464 - | Plugin manifest format (plugin.toml) | Done | Standardized plugin metadata and configuration |
465 - | Plugin loader and manager UI | Done | Enable/disable plugins, manage installed plugins |
466 - | Import plugin trait and execution | Done | Plugins can import data from external sources |
467 - | CSV import reference plugin | Done | Example plugin demonstrating the import pipeline |
468 - | Import wizard UI (3-step flow) | Done | File selection, preview parsed data, execute import |
469 - | goingson:: API module (read_file, parse_csv, parse_json) | Done | Plugin API for reading and parsing data |
470 - | Export adapters | Planned | Plugins that export data in custom formats |
471 - | Custom commands | Planned | Plugins that add new Tauri commands |
472 - | Lifecycle hooks (on_task_created, on_email_received, etc.) | Planned | Plugins that react to app events |
473 - | Hot-reload on .rhai file changes | Planned | Edit plugins and see changes without restart |
474 - | Install from URL/file | Planned | Download and install community plugins |
475 - | TaskWarrior JSON import plugin | Planned | Migrate from TaskWarrior |
476 - | Todoist API import plugin | Planned | Migrate from Todoist |
477 - | Things 3 import plugin (macOS) | Planned | Migrate from Things 3 |
478 - | Apple Reminders import plugin | Planned | Migrate from Apple Reminders |
479 - | Notion database import plugin | Planned | Migrate from Notion |
480 - | Trello board import plugin | Planned | Migrate from Trello |
481 - | Google Tasks import plugin | Planned | Migrate from Google Tasks |
482 - | CSV export plugin | Planned | Export tasks with configurable columns |
483 - | JSON export (full data dump) | Done | Export all data as JSON for backup/migration |
484 - | Markdown export (task lists) | Planned | Export tasks as markdown checklists |
485 - | ICS calendar export (enhanced) | Done | Export events to standard ICS format |
486 - | Obsidian markdown export | Planned | Export into Obsidian-compatible vault format |
487 -
488 - ### AI / Agent Integration
489 -
490 - | Feature | Status | Use Case |
491 - |---------|--------|----------|
492 - | MCP (Model Context Protocol) server | Done | LLM agents (e.g., Claude Code) can read/write tasks, projects, events via MCP tools |
493 - | Full MCP tool suite (40+ tools) | Done | Complete CRUD on all entities, dashboard stats, search, export from any MCP-compatible agent |
494 - | GUI change detection (DB file watcher) | Done | App auto-refreshes when an external agent modifies the database |
495 - | LLM dynamic templates ({: prompt :} syntax) | Done | Template fields re-evaluated at display time by LLM (e.g., daily motivational quote) |
496 - | LLM static templates / AI-Fill button ((: prompt :) syntax) | Done | Click to fill a form field with LLM-generated content |
497 - | Ollama / OpenAI-compatible provider support | Done | Connect to local or remote LLMs |
498 - | Response caching with date-based invalidation | Done | Avoid redundant LLM calls for the same prompt |
499 - | In-app agent tab with chat interface | Planned | Chat with an AI agent directly inside the app |
500 - | Message history persistence | Planned | Persistent conversation history with the agent |
Lines truncated
@@ -1,137 +0,0 @@
1 - # GoingsOn — Product Description Draft
2 -
3 - > Outline of features for MNW listing. Replace prose with real copy.
4 -
5 - ## One-Liner
6 -
7 - Email, calendar, tasks in one place. Project management for individuals and small teams.
8 -
9 - ## Description
10 -
11 - <!-- Your copy here -->
12 -
13 - ## Features
14 -
15 - ### Project Management
16 - - Create projects with types (Job, Side Project, Company, Essay, Article, Painting, Other)
17 - - Project statuses (Active, Inactive, Archived)
18 - - Per-project dashboard showing tasks, events, and emails in columns
19 - - Milestones with target dates, ordering, and completion tracking
20 -
21 - ### Task Management
22 - - TaskWarrior-inspired with automatic urgency scoring (priority + due date proximity)
23 - - Quick-add with natural language parsing ("Fix bug due:tomorrow +H @ProjectName")
24 - - Priorities: High, Medium, Low
25 - - Statuses: Pending, Started, Completed, Deleted
26 - - Due dates with overdue tracking
27 - - Subtasks (checkbox items within tasks, can link to other full tasks for multi-phase work)
28 - - Timestamped annotations (notes attached to tasks)
29 - - Recurrence: Daily, Weekly, Monthly, Yearly (completing creates next instance automatically)
30 - - Snooze tasks until Later Today, Tomorrow, Weekend, or Next Week
31 - - Mark tasks as "waiting for response" with visual indicator
32 - - Filtering by status, project, priority, milestone, snoozed, waiting
33 - - Sorting by description, project, priority, due date, urgency
34 - - Bulk operations: complete, snooze, delete multiple tasks at once
35 - - Pagination for large task lists
36 -
37 - ### Day Planning (Time Blocking)
38 - - Visual hourly timeline
39 - - Drag unscheduled tasks onto the timeline to create time blocks
40 - - Move tasks between time slots
41 - - Events display alongside time blocks
42 - - Current time indicator
43 - - Collapsible sidebar with unscheduled tasks
44 - - Navigate days with keyboard ([ and ])
45 -
46 - ### Weekly Review
47 - - Review completed tasks from the past week
48 - - Set focus tasks for the current week
49 - - Track vacation days
50 - - Nudge system when review is overdue
51 -
52 - ### Email (IMAP/SMTP + Fastmail OAuth/JMAP)
53 - - Connect any IMAP/SMTP email account
54 - - Fastmail OAuth with JMAP protocol support
55 - - Background sync on configurable interval
56 - - Threaded email display
57 - - HTML email rendering (opens in browser)
58 - - Compose and send (separate window on desktop)
59 - - Reply with correct threading headers (In-Reply-To)
60 - - Mark read/unread, archive/unarchive
61 - - Snooze emails (reappear after snooze time)
62 - - Mark as "waiting for response"
63 - - Create task directly from an email
64 - - Link emails to projects
65 - - Unread count badge (macOS dock icon)
66 - - Bulk operations: mark read, archive, snooze, delete
67 -
68 - ### Calendar / Events
69 - - Create events with title, description, start/end time, location
70 - - Link events to projects and contacts
71 - - Recurrence: Daily, Weekly, Monthly, Yearly
72 - - Upcoming events list with collapsible past events
73 - - Events display on day plan timeline
74 -
75 - ### Contacts
76 - - Store display name, nickname, company, title, notes, birthday, timezone
77 - - Multiple emails, phone numbers, social handles, and custom fields per contact
78 - - Tag contacts for filtering
79 - - Search by name or email
80 - - Link contacts to events
81 -
82 - ### Search
83 - - Full-text search across tasks, projects, emails, and contacts (FTS5)
84 - - Click result to navigate directly to item
85 -
86 - ### Saved Views
87 - - Save any filter configuration as a named view
88 - - Pin views for quick access
89 - - Edit and delete saved views
90 -
91 - ### Import / Export
92 - - Import from CSV (tasks, projects, contacts) via Rhai plugin system
93 - - Export full database as JSON
94 - - Export tasks as CSV
95 - - Export events as ICS (standard calendar format)
96 - - Manual and auto-scheduled backups with configurable retention
97 - - Restore from any backup
98 -
99 - ### LLM Integration
100 - - Connect to Ollama, OpenAI, or custom LLM providers
101 - - Template expansion in task descriptions
102 - - Test connection from settings
103 - - Response caching
104 -
105 - ### MCP Server (Claude Desktop Integration)
106 - - Manage tasks via Claude Desktop using MCP protocol
107 - - Available tools: list tasks, create, complete, delete, snooze, search, export roadmap
108 - - External changes auto-detected via database file watcher
109 -
110 - ### Themes
111 - - Follow System (auto light/dark)
112 - - Light: Default, Sandstone, Mint
113 - - Dark: Midnight, Space
114 -
115 - ### Keyboard Shortcuts
116 - - Full vim-style navigation (j/k for items, g+key for views)
117 - - Quick add (q), new item (n), complete (c), archive (a), snooze (s), schedule (S)
118 - - ? to show shortcuts overlay
119 - - Escape closes any modal or overlay
120 -
121 - ### Desktop Notifications
122 - - Snooze expiry alerts
123 - - Event proximity alerts
124 - - New email notifications
125 -
126 - ### Platforms
127 - - macOS (primary)
128 - - Windows, Linux
129 - - iOS (in development, simulator working)
130 -
131 - ## Price
132 -
133 - Free (alpha)
134 -
135 - ## Tags
136 -
137 - Productivity, Tasks, Email, Calendar, Project Management, Desktop App, Time Blocking
@@ -1,33 +0,0 @@
1 - # GoingsOn -- Documentation Review
2 -
3 - **Last reviewed:** 2026-03-04 (first doc audit)
4 -
5 - ## Overall Grade: B+
6 -
7 - Good coverage of architecture and style guide. CLAUDE.md GO section is thorough and accurate (after fixes applied this audit). No public-facing docs to worry about. Main gaps: description.md is a placeholder, and MCP_test.md is test scaffolding that could be removed.
8 -
9 - ## Document Heatmap
10 -
11 - | Document | Status | Last Verified | Notes |
12 - |----------|:------:|:-------------:|-------|
13 - | CLAUDE.md (GO section) | Fixed | 2026-03-04 | `models.rs` -> `models/`, `savedViews` -> `dayPlan` in namespace list |
14 - | docs/ARCHITECTURE.md | Current | 2026-03-04 | Accurate to codebase |
15 - | docs/STYLEGUIDE.md | Current | 2026-03-04 | Skeubrute design system, referenced from CLAUDE.md |
16 - | docs/about.md | Current | 2026-03-04 | Project overview |
17 - | docs/competition.md | Current | 2026-03-04 | Competitive analysis |
18 - | docs/description.md | Placeholder | 2026-03-04 | Intentional placeholder, not stale |
19 - | docs/human_testing.md | Current | 2026-03-04 | Manual QA checklist |
20 - | docs/audit_review.md | Current | 2026-03-04 | Code audit history |
21 - | docs/MCP_test.md | Low priority | 2026-03-04 | Test artifact, may not need to persist |
22 -
23 - ## Stale References Found (This Audit)
24 -
25 - | Location | Issue | Resolution |
26 - |----------|-------|------------|
27 - | CLAUDE.md | `models.rs` listed as file, is `models/` directory | Fixed |
28 - | CLAUDE.md | `savedViews` in namespace, doesn't exist | Replaced with `dayPlan` |
29 -
30 - ## Action Items
31 -
32 - - None critical. All issues found this audit have been fixed.
33 - - Consider removing `docs/MCP_test.md` if no longer needed.
@@ -1,339 +0,0 @@
1 - # GoingsOn — Pre-Launch Manual Testing
2 -
3 - ## How to Test
4 -
5 - - GoingsOn is a Tauri 2 desktop app — manual testing means using the app as a real user would
6 - - Automated tests cover core logic but can't catch visual bugs, broken interactions, or UX regressions
7 - - Work through each section sequentially, checking boxes as you go
8 - - If something fails, note the issue inline and keep going
9 - - Prioritized: P0 (app is broken without these), P1 (core features), P2 (edge cases + polish)
10 -
11 - ### Environment Setup
12 -
13 - - [ ] App built and running (`cargo tauri dev` or release build)
14 - - [ ] SQLite database initialized (migrations auto-run on launch)
15 - - [ ] Email account configured (IMAP/SMTP or Fastmail OAuth) if testing email features
16 - - [ ] Second terminal open watching Tauri logs for errors
17 -
18 - ### Tips
19 -
20 - - Watch the Tauri dev console (Cmd+Option+I) for JS errors
21 - - Test keyboard shortcuts alongside mouse interactions — both paths should work
22 - - After destructive actions (delete, bulk ops), verify the data is actually gone, not just hidden
23 - - Test in a fresh database occasionally to catch first-run issues
24 -
25 - ---
26 -
27 - ## P0 — Critical Path
28 -
29 - > If any of these fail, do not ship.
30 -
31 - ### App Launch + First Run
32 -
33 - - [ ] App opens without crash or panic
34 - - [ ] Window renders at correct default size
35 - - [ ] Empty state is sensible (no blank screen, shows guidance or empty lists)
36 - - [ ] Database created automatically in app data directory
37 - - [ ] All tabs are accessible: Projects, Tasks, Emails, Day Plan, Weekly Review, Events, Contacts
38 -
39 - ### Project CRUD
40 -
41 - - [ ] Create project — appears in project list
42 - - [ ] All project types available (Job, SideProject, Company, Essay, Article, Painting, Other)
43 - - [ ] All statuses work (Active, Inactive, Archived)
44 - - [ ] Edit project — name, description, type, status save correctly
45 - - [ ] Delete project — removed from list, associated tasks/events cleaned up
46 - - [ ] Project dashboard renders with tasks/events/emails columns
47 -
48 - ### Task CRUD + Lifecycle
49 -
50 - - [ ] Create task — appears in task list
51 - - [ ] Quick add (`q` shortcut) — natural language parsing works (e.g., "Fix bug due:tomorrow +H @ProjectName")
52 - - [ ] Edit task — all fields save (description, priority, due date, project, tags)
53 - - [ ] Start task — status changes to Started
54 - - [ ] Complete task — status changes to Completed, disappears from active view
55 - - [ ] Delete task — removed from list
56 - - [ ] Priority levels work (High, Medium, Low) and display correctly
57 - - [ ] Due dates display and sort correctly
58 - - [ ] Urgency calculation reflects priority + due date proximity
59 -
60 - ### Task Subtasks
61 -
62 - - [ ] Add subtask to task — appears in task detail
63 - - [ ] Toggle subtask completion — checkbox state persists
64 - - [ ] Update subtask text — saves correctly
65 - - [ ] Delete subtask — removed
66 - - [ ] Link subtask to another task (multi-phase) — link navigable
67 -
68 - ### Task Annotations
69 -
70 - - [ ] Add annotation — timestamped note appears
71 - - [ ] Delete annotation — removed
72 -
73 - ### Navigation + View Switching
74 -
75 - - [ ] Tab bar switches between all views
76 - - [ ] Keyboard shortcuts for navigation: `g t` (tasks), `g e` (emails), `g p` (projects), `g v` (events), `g d` (day plan)
77 - - [ ] `?` shows keyboard shortcuts overlay
78 - - [ ] `Escape` closes modals and overlays
79 - - [ ] Back/forward navigation within project dashboard
80 -
81 - ---
82 -
83 - ## P1 — Core Features
84 -
85 - ### Task Filtering + Sorting
86 -
87 - - [ ] Filter by status (Pending, Started, Completed, Deleted)
88 - - [ ] Filter by project
89 - - [ ] Filter by priority
90 - - [ ] Filter by milestone
91 - - [ ] Filter snoozed tasks
92 - - [ ] Filter waiting tasks
93 - - [ ] Sort by description, project, priority, due date, urgency
94 - - [ ] Sort direction toggles correctly
95 - - [ ] Combined filters work (e.g., High priority + specific project)
96 - - [ ] Pagination works for large task lists
97 -
98 - ### Task Snooze
99 -
100 - - [ ] Snooze task — disappears from active list
101 - - [ ] Snooze options render (Later Today, Tomorrow, Weekend, Next Week)
102 - - [ ] Snoozed task reappears after snooze time passes
103 - - [ ] Unsnooze task manually — reappears immediately
104 - - [ ] List snoozed tasks shows all snoozed items
105 -
106 - ### Task Waiting
107 -
108 - - [ ] Mark task as waiting for response — visual indicator shown
109 - - [ ] Clear waiting status — indicator removed
110 - - [ ] List waiting tasks shows all waiting items
111 -
112 - ### Task Recurrence
113 -
114 - - [ ] Create recurring task (Daily, Weekly, Monthly, Yearly)
115 - - [ ] Complete recurring task — next instance auto-created with correct date
116 - - [ ] Recurrence indicator visible in task list
117 - - [ ] Parent-child linkage intact
118 -
119 - ### Events
120 -
121 - - [ ] Create event — title, description, start/end time, location
122 - - [ ] Edit event — all fields save
123 - - [ ] Delete event — removed
124 - - [ ] Upcoming events list shows future events
125 - - [ ] Past events collapsible
126 - - [ ] Link event to project — appears in project dashboard
127 - - [ ] Link event to contact
128 - - [ ] Event recurrence works (daily, weekly, monthly, yearly)
129 -
130 - ### Day Planning
131 -
132 - - [ ] Day plan timeline renders with hourly slots
133 - - [ ] Drag unscheduled tasks to timeline — creates time block
134 - - [ ] Move tasks between time slots
135 - - [ ] Events display on timeline
136 - - [ ] Current time indicator visible and accurate
137 - - [ ] Sidebar shows unscheduled tasks
138 - - [ ] Navigate days: `[` (previous), `]` (next)
139 - - [ ] Schedule task via `S` keyboard shortcut
140 - - [ ] Unschedule task — returns to unscheduled list
141 -
142 - ### Weekly Review
143 -
144 - - [ ] Weekly review renders for current week
145 - - [ ] Shows completed tasks from past week
146 - - [ ] Set focus tasks for the week
147 - - [ ] Clear all focus — resets focus flags
148 - - [ ] Set vacation days
149 - - [ ] Complete weekly review — marks as done
150 - - [ ] Review nudge appears when review is overdue
151 -
152 - ### Email — Account Setup
153 -
154 - - [ ] Create IMAP/SMTP account — all fields save (server, port, credentials)
155 - - [ ] Test connection — reports success or specific error
156 - - [ ] Create Fastmail OAuth account — OAuth flow completes
157 - - [ ] OAuth token refresh works (simulate expired token)
158 - - [ ] Update sync interval — persists
159 - - [ ] Delete email account — account and synced emails removed
160 -
161 - ### Email — Sync + Display
162 -
163 - - [ ] Manual sync (`sync_email_account`) — fetches new emails
164 - - [ ] Background sync fires on interval
165 - - [ ] Email list renders with sender, subject, date
166 - - [ ] Threaded view groups related emails
167 - - [ ] HTML email renders correctly (open in browser)
168 - - [ ] Unread count updates in real-time
169 - - [ ] Pagination works for large mailboxes
170 -
171 - ### Email — Actions
172 -
173 - - [ ] Mark read / unread — state persists and syncs
174 - - [ ] Archive / unarchive — moves between views
175 - - [ ] Mark all read — bulk operation works
176 - - [ ] Delete email — removed
177 - - [ ] Link email to project — appears in project dashboard
178 - - [ ] Snooze email — disappears, reappears after snooze time
179 - - [ ] Mark email as waiting for response
180 - - [ ] Create task from email (`t` shortcut) — task created with context
181 -
182 - ### Email — Compose + Send
183 -
184 - - [ ] Compose window opens (desktop: separate window, mobile: modal)
185 - - [ ] Send email via SMTP — delivers to recipient
186 - - [ ] Reply maintains thread (In-Reply-To header set)
187 - - [ ] Sent email appears in email list as outgoing
188 -
189 - ### Contacts
190 -
191 - - [ ] Create contact — display name, company, title, notes, tags
192 - - [ ] Add multiple emails to contact
193 - - [ ] Add phone numbers
194 - - [ ] Add social handles
195 - - [ ] Add custom fields
196 - - [ ] Edit contact — all fields save
197 - - [ ] Delete contact — removed
198 - - [ ] Filter contacts by tag
199 - - [ ] Search contacts by name/email
200 - - [ ] Find contact by email address — correct contact returned
201 - - [ ] Contact linked to events displays correctly
202 -
203 - ### Milestones
204 -
205 - - [ ] Create milestone in project — name, description, target date
206 - - [ ] Edit milestone — fields save
207 - - [ ] Delete milestone — removed, tasks unlinked
208 - - [ ] Reorder milestones — order persists
209 - - [ ] Complete milestone — status changes
210 - - [ ] Filter tasks by milestone
211 -
212 - ### Search
213 -
214 - - [ ] Global search (`search`) returns results across tasks, projects, emails, contacts
215 - - [ ] Results are relevant (full-text search, not just prefix)
216 - - [ ] Clicking search result navigates to correct item
217 -
218 - ### Saved Views
219 -
220 - - [ ] Create saved view with filter configuration
221 - - [ ] Load saved view — filters apply correctly
222 - - [ ] Pin view — appears in pinned views list
223 - - [ ] Unpin view — removed from pinned list
224 - - [ ] Edit saved view — filters update
225 - - [ ] Delete saved view — removed
226 -
227 - ### Bulk Operations
228 -
229 - - [ ] Select multiple tasks (Cmd+Click, Shift+Click)
230 - - [ ] Select all (Cmd+A)
231 - - [ ] Bulk complete — all selected tasks completed
232 - - [ ] Bulk snooze — all selected tasks snoozed
233 - - [ ] Bulk delete — all selected tasks deleted
234 - - [ ] Select multiple emails (Cmd+Click, Shift+Click)
235 - - [ ] Bulk mark read — all selected emails marked
236 - - [ ] Bulk archive — all selected emails archived
237 - - [ ] Bulk delete emails — all selected removed
238 -
239 - ### Themes
240 -
241 - - [ ] Follow System — respects OS dark/light mode
242 - - [ ] Light themes: default, sandstone, mint — each renders correctly
243 - - [ ] Dark themes: midnight, space — each renders correctly
244 - - [ ] Theme persists after restart
245 -
246 - ---
247 -
248 - ## P2 — Edge Cases + Polish
249 -
250 - ### Keyboard Navigation
251 -
252 - - [ ] `j` / `k` navigates items in all list views
253 - - [ ] `Enter` opens selected item
254 - - [ ] `n` creates new item in current view
255 - - [ ] `a` archives email
256 - - [ ] `c` completes task
257 - - [ ] `s` snoozes item
258 - - [ ] All shortcuts listed in `?` overlay work
259 -
260 - ### Data Integrity
261 -
262 - - [ ] Create item → restart app → item persists
263 - - [ ] Delete cascades: project deletion removes tasks, events
264 - - [ ] Recurring task completion doesn't duplicate or lose data
265 - - [ ] Email threading doesn't create phantom threads
266 - - [ ] Large datasets (100+ tasks, 500+ emails) — no performance degradation
267 -
268 - ### Empty States + Error Handling
269 -
270 - - [ ] Empty project list — shows helpful message
271 - - [ ] Empty task list — shows helpful message
272 - - [ ] Empty email inbox — shows helpful message
273 - - [ ] No email accounts configured — shows setup prompt
274 - - [ ] Email sync failure — shows error, doesn't crash
275 - - [ ] Invalid task input (empty description) — rejected gracefully
276 - - [ ] Database locked (concurrent access) — handled gracefully
277 -
278 - ### Import
279 -
280 - - [ ] Import CSV tasks — preview renders correctly
281 - - [ ] Execute import — tasks created with correct fields
282 - - [ ] Plugin enable/disable — toggles work
283 - - [ ] Import with malformed data — errors reported, valid rows imported
284 -
285 - ### Export + Backup
286 -
287 - - [ ] Export JSON — full database dump, valid JSON
288 - - [ ] Export tasks CSV — correct columns and data
289 - - [ ] Export events ICS — valid calendar file, importable elsewhere
290 - - [ ] Create backup — timestamped file created
291 - - [ ] List backups — shows available backups
292 - - [ ] Restore backup — app state reverts to backup point
293 - - [ ] Delete backup — file removed
294 - - [ ] Auto-backup settings save and trigger on interval
295 -
296 - ### LLM Integration
297 -
298 - - [ ] Configure Ollama provider — settings save
299 - - [ ] Test connection — reports success or error
300 - - [ ] Template expansion works in task descriptions (if Ollama running)
301 - - [ ] Clear LLM cache — no stale responses
302 -
303 - ### MCP Server
304 -
305 - - [ ] MCP server responds to tool calls (test via Claude Desktop or direct)
306 - - [ ] `get_context` returns current state
307 - - [ ] `list_tasks` with filters returns correct results
308 - - [ ] `create_task` creates task visible in app (database watcher triggers reload)
309 - - [ ] `complete_task` marks task done
310 -
311 - ### Window + Display
312 -
313 - - [ ] Resize window — layout adapts, no clipping or overflow
314 - - [ ] Minimum window size enforced
315 - - [ ] Modals centered and dismissible
316 - - [ ] Context menus position correctly (don't clip off-screen)
317 - - [ ] Toasts appear and auto-dismiss
318 -
319 - ### Notifications (Desktop)
320 -
321 - - [ ] Snooze notification fires when snooze expires
322 - - [ ] Event proximity alert fires before event
323 - - [ ] Email sync notification on new emails
324 - - [ ] macOS dock badge shows unread email count
325 -
326 - ---
327 -
328 - ## Sign-Off
329 -
330 - | Field | Value |
331 - |-------|-------|
332 - | Date | |
333 - | Tester | |
334 - | Platform | macOS / Windows / Linux |
335 - | Build type | dev / release |
336 - | P0 result | pass / fail |
337 - | P1 result | pass / fail |
338 - | P2 result | pass / fail / skipped |
339 - | Notes | |
@@ -1,541 +0,0 @@
1 - # Co-Working Feature Plan
2 -
3 - A collaborative workspace feature for GoingsOn with end-to-end encryption, enabling teams and partners to share projects, tasks, and schedules while maintaining privacy.
4 -
5 - ---
6 -
7 - ## Vision
8 -
9 - Enable seamless collaboration without sacrificing the privacy-first, local-first nature of GoingsOn. Users can:
10 - - Share specific projects or views with collaborators
11 - - See teammates' availability without exposing private details
12 - - Collaborate on shared tasks in real-time
13 - - Trust that shared data is encrypted end-to-end
14 -
15 - ---
16 -
17 - ## Core Concepts
18 -
19 - ### UUID-Based Sharing Model
20 -
21 - **Projects live at unique URLs.** Each shared project has a UUID that becomes its permanent address:
22 -
23 - ```
24 - goingson://project/{project-uuid}
25 - https://go.goingson.app/p/{project-uuid}
26 - ```
27 -
28 - **Sharing = generating a unique link per person.** When you share a project:
29 - 1. You generate a unique **share link UUID** for each collaborator
30 - 2. That share link UUID identifies them for all their activity
31 - 3. Changes they make are associated with their share link UUID
32 - 4. Revoking access = invalidating their share link UUID
33 -
34 - ```
35 - Share link for Alice: goingson://join/{share-uuid-alice}
36 - Share link for Bob: goingson://join/{share-uuid-bob}
37 - ```
38 -
39 - **No accounts required.** The share link IS the identity. The person who has the link is the collaborator. If Alice shares her link, whoever uses it becomes "Alice" in that project.
40 -
41 - ### Shareable Units
42 -
43 - | Unit | Description | Use Case |
44 - |------|-------------|----------|
45 - | **Project** | Share an entire project with all its tasks | Team projects, client work |
46 - | **View** | Share a saved view (filtered task list) | Sprint boards, focus lists |
47 - | **Calendar Overlay** | Share busy/free times (not details) | Scheduling, availability |
48 - | **Task** | Share individual tasks | Delegation, handoffs |
49 -
50 - ### Permission Levels
51 -
52 - | Level | Can View | Can Edit | Can Manage Members |
53 - |-------|----------|----------|-------------------|
54 - | Viewer | Yes | No | No |
55 - | Collaborator | Yes | Yes | No |
56 - | Admin | Yes | Yes | Yes |
57 - | Owner | Yes | Yes | Yes (transfer ownership) |
58 -
59 - ### Sharing Flow
60 -
61 - 1. **Owner creates project** → Project gets a UUID
62 - 2. **Owner shares with Alice** → Generate share-link UUID for Alice with permissions
63 - 3. **Alice opens link** → App registers her share-link UUID locally, syncs project
64 - 4. **Alice edits task** → Change tagged with her share-link UUID
65 - 5. **Owner sees "Alice updated task"** → Identified by her share-link's display name
66 - 6. **Owner revokes Alice** → Her share-link UUID is invalidated, she loses access
67 -
68 - ### Why UUID-Based Sharing?
69 -
70 - This approach has several advantages over traditional account-based sharing:
71 -
72 - | Benefit | Description |
73 - |---------|-------------|
74 - | **No accounts needed** | Recipients don't need to create an account. The link IS their identity. |
75 - | **Per-person revocation** | Revoke one person without affecting others. Generate new link to re-invite. |
76 - | **Simple mental model** | "Send this link to Alice" is easier than "invite alice@email.com" |
77 - | **Attribution without accounts** | Every change is tied to a share link, owner sets display name |
78 - | **Forwarding = trust transfer** | If Alice shares her link, that's her choice. The recipient becomes "Alice". |
79 - | **Multiple personas** | Same person can have multiple links with different permissions/names |
80 - | **Offline-first** | No server round-trip needed to create share links |
81 -
82 - ---
83 -
84 - ## End-to-End Encryption Design
85 -
86 - ### Key Hierarchy
87 -
88 - ```
89 - User Master Key (derived from password via Argon2)
90 - └── User Private Key (Ed25519/X25519)
91 - └── Share Keys (per shared unit, AES-256-GCM)
92 - └── Encrypted Content
93 - ```
94 -
95 - ### Key Exchange Flow (UUID-Based)
96 -
97 - ```
98 - 1. Owner creates a shared project
99 - 2. Owner generates a Project Share Key (random AES-256 key)
100 - 3. Owner encrypts Share Key with their private key (for recovery)
101 -
102 - When generating a share link for Bob:
103 - 4. Owner generates a random Link Key Material
104 - 5. Owner encrypts Project Share Key with Link Key Material
105 - 6. Owner stores encrypted key in share_links table
106 - 7. Share link URL contains Link Key Material in fragment: /j/{uuid}#{key-material}
107 -
108 - When Bob opens the link:
109 - 8. Bob extracts Link Key Material from URL fragment
110 - 9. Bob fetches encrypted Project Share Key from relay (using share-link UUID)
111 - 10. Bob decrypts Project Share Key using Link Key Material
112 - 11. Bob can now encrypt/decrypt project content
113 -
114 - Key insight: No public key exchange needed! The key material is in the URL.
115 - ```
116 -
117 - ### What Gets Encrypted
118 -
119 - | Data | Encrypted | Notes |
120 - |------|-----------|-------|
121 - | Task descriptions | Yes | Full E2EE |
122 - | Task metadata (dates, priority) | Yes | Needed for sorting/filtering |
123 - | Project names | Yes | |
124 - | File attachments | Yes | Encrypt before upload |
125 - | User identities | Partially | Pseudonymous IDs visible, real names encrypted |
126 - | Sync timestamps | No | Needed for conflict resolution |
127 -
128 - ### Crypto Libraries
129 -
130 - - **libsodium** (via `sodiumoxide` crate) - Battle-tested, audited
131 - - **age** - Simple file encryption for attachments
132 - - Key derivation: Argon2id
133 - - Symmetric: XChaCha20-Poly1305
134 - - Asymmetric: X25519 + Ed25519
135 -
136 - ---
137 -
138 - ## Architecture
139 -
140 - ### Option A: Relay Server (Recommended for MVP)
141 -
142 - ```
143 - ┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
144 - │ Alice │ ──E2EE──│ Relay Server │──E2EE── │ Bob │
145 - │ (Tauri) │ │ (sees nothing) │ │ (Tauri) │
146 - └─────────────┘ └─────────────────┘ └─────────────┘
147 - ```
148 -
149 - - Server stores encrypted blobs, cannot read content
150 - - Handles presence, sync, and message routing
151 - - Simple deployment (single service)
152 - - Users authenticate with signed challenges (no passwords on server)
153 -
154 - ### Option B: Peer-to-Peer (Future)
155 -
156 - ```
157 - ┌─────────────┐ ┌─────────────┐
158 - │ Alice │ ←───── Direct E2EE ─────→ │ Bob │
159 - │ (Tauri) │ (WebRTC/QUIC) │ (Tauri) │
160 - └─────────────┘ └─────────────┘
161 - ```
162 -
163 - - No server needed for sync
164 - - Works offline between local network peers
165 - - More complex NAT traversal
166 - - Could use relay as fallback/signaling
167 -
168 - ### Data Flow
169 -
170 - ```
171 - Local SQLite → Encrypt → Sync Queue → Relay Server → Recipient
172 -
173 - Decrypt ← Sync Queue ← Local SQLite
174 - ```
175 -
176 - ### Conflict Resolution
177 -
178 - Use CRDTs (Conflict-free Replicated Data Types) for shared data:
179 - - **LWW-Register** (Last-Writer-Wins) for simple fields
180 - - **OR-Set** for tags, collaborators
181 - - **RGA** for ordered lists (subtasks)
182 -
183 - Libraries: `yrs` (Yjs port to Rust) or `automerge-rs`
184 -
185 - ---
186 -
187 - ## Database Schema Extensions
188 -
189 - ### UUID-Centric Design
190 -
191 - The schema is built around two core UUIDs:
192 - - **Project UUID**: Permanent identifier for the shared project
193 - - **Share Link UUID**: Unique identifier per collaborator (their "ticket" to access)
194 -
195 - ```sql
196 - -- Local user identity (for signing/encryption)
197 - CREATE TABLE user_identity (
198 - id TEXT PRIMARY KEY,
199 - public_key BLOB NOT NULL, -- X25519 public key
200 - signing_key BLOB NOT NULL, -- Ed25519 public key
201 - encrypted_private_key BLOB NOT NULL, -- Encrypted with master key
202 - created_at TEXT NOT NULL
203 - );
204 -
205 - -- Shared projects (projects that can be collaborated on)
206 - CREATE TABLE shared_projects (
207 - project_id TEXT PRIMARY KEY, -- The project's UUID (same as projects.id)
208 - share_key BLOB NOT NULL, -- Encrypted symmetric key for this project's data
209 - is_owner INTEGER NOT NULL, -- 1 if we own this project, 0 if shared with us
210 - created_at TEXT NOT NULL,
211 - FOREIGN KEY (project_id) REFERENCES projects(id)
212 - );
213 -
214 - -- Share links: one per collaborator per project
215 - -- The share_link_id IS the collaborator's identity for this project
216 - CREATE TABLE share_links (
217 - id TEXT PRIMARY KEY, -- The share link UUID (shared with collaborator)
218 - project_id TEXT NOT NULL, -- Which project this grants access to
219 - display_name TEXT NOT NULL, -- Human-readable name ("Alice", "Bob")
220 - permission TEXT NOT NULL, -- 'viewer', 'collaborator', 'admin'
221 - encrypted_share_key BLOB NOT NULL, -- Project's share key, encrypted for this link
222 - created_at TEXT NOT NULL,
223 - expires_at TEXT, -- Optional expiration
224 - revoked_at TEXT, -- Null if active, timestamp if revoked
225 - last_seen_at TEXT, -- Last sync from this collaborator
226 - FOREIGN KEY (project_id) REFERENCES projects(id)
227 - );
228 - CREATE INDEX idx_share_links_project ON share_links(project_id);
229 -
230 - -- Track which share link we're using for projects shared with us
231 - CREATE TABLE my_share_links (
232 - project_id TEXT PRIMARY KEY, -- Project we have access to
233 - share_link_id TEXT NOT NULL, -- The share link UUID we were given
234 - display_name TEXT NOT NULL, -- Name we show as (what owner sees)
235 - encrypted_share_key BLOB NOT NULL, -- Decrypted with link-specific key derivation
236 - joined_at TEXT NOT NULL,
237 - FOREIGN KEY (project_id) REFERENCES projects(id)
238 - );
239 -
240 - -- Change log: who did what
241 - -- Attribution via share_link_id (not user accounts)
242 - CREATE TABLE change_log (
243 - id TEXT PRIMARY KEY,
244 - project_id TEXT NOT NULL,
245 - share_link_id TEXT, -- Who made the change (null = local owner)
246 - entity_type TEXT NOT NULL, -- 'task', 'event', etc.
247 - entity_id TEXT NOT NULL,
248 - operation TEXT NOT NULL, -- 'create', 'update', 'delete'
249 - encrypted_diff BLOB NOT NULL, -- What changed (encrypted)
250 - vector_clock TEXT NOT NULL, -- For CRDT ordering
251 - created_at TEXT NOT NULL,
252 - synced_at TEXT, -- When pushed/pulled to/from relay
253 - FOREIGN KEY (project_id) REFERENCES projects(id)
254 - );
255 - CREATE INDEX idx_change_log_project ON change_log(project_id);
256 - CREATE INDEX idx_change_log_sync ON change_log(synced_at);
257 - ```
258 -
259 - ### How Attribution Works
260 -
261 - When a change comes in from the relay:
262 - 1. Change includes the `share_link_id` that made it
263 - 2. Look up `share_links` table to get `display_name`
264 - 3. Display as "Alice updated task" (where "Alice" is the display_name)
265 -
266 - When we make a change to a shared-with-us project:
267 - 1. Look up our `share_link_id` from `my_share_links`
268 - 2. Attach it to the change
269 - 3. Owner sees changes attributed to our display_name
270 -
271 - ### URL Structure
272 -
273 - ```
274 - Project URL (permanent):
275 - goingson://project/{project-uuid}
276 - https://go.goingson.app/p/{project-uuid}
277 -
278 - Share Link URL (per-person):
279 - goingson://join/{share-link-uuid}
280 - https://go.goingson.app/j/{share-link-uuid}
281 -
282 - The share link URL encodes:
283 - - Which project to access
284 - - What permissions they have
285 - - Their identity for attribution
286 - - The decryption key (in URL fragment, not sent to server)
287 - ```
288 -
289 - ### Share Link URL Format
290 -
291 - ```
292 - https://go.goingson.app/j/{share-link-uuid}#{key-material}
293 -
294 - Where:
295 - - share-link-uuid: Identifies the share link in the database
296 - - key-material: Base64-encoded key derivation material (after #, not sent to server)
297 - ```
298 -
299 - This allows:
300 - - Server only sees the share-link-uuid (can't decrypt content)
301 - - Recipient uses key-material to derive their decryption key
302 - - Revoking = deleting the share_link row (key-material becomes useless)
303 -
304 - ---
305 -
306 - ## User Experience
307 -
308 - ### Sharing a Project
309 -
310 - 1. Right-click project → "Share..."
311 - 2. Modal shows:
312 - - **Project URL**: `goingson.app/p/{uuid}` (permanent address)
313 - - **Generate Link for...**: Input name, select permission, generate unique link
314 - - **Active Links**: List of all share links with names, permissions, last seen
315 - 3. Click "Generate Link" → Creates unique share link UUID
316 - 4. Copy link → Send to collaborator via any channel
317 - 5. Collaborator opens link → GoingsOn app opens, project syncs
318 - 6. Collaborator appears in "Active Links" with their display name
319 -
320 - ### Revoking Access
321 -
322 - 1. Open share modal for project
323 - 2. Find collaborator in "Active Links"
324 - 3. Click "Revoke" → Their share link UUID is invalidated
325 - 4. Next time they sync, access is denied
326 - 5. Generate new link if they need access again (new UUID)
327 -
328 - ### Shared Project UI
329 -
330 - ```
331 - ┌─────────────────────────────────────────────────────┐
332 - │ [Shared] Project: Website Redesign [3 👤] │
333 - ├─────────────────────────────────────────────────────┤
334 - │ ┌─────────────────────────────────────────────────┐ │
335 - │ │ ● Alice is viewing ● Bob is editing task #3 │ │
336 - │ └─────────────────────────────────────────────────┘ │
337 - │ │
338 - │ [ ] Design homepage mockup @Alice Due Mon │
339 - │ └─ Alice added 2h ago │
340 - │ [✓] Set up dev environment @Bob Done │
341 - │ └─ Bob completed just now │
342 - │ [ ] Review color palette — Due Wed │
343 - │ └─ You created yesterday │
344 - └─────────────────────────────────────────────────────┘
345 - ```
346 -
347 - Features:
348 - - Presence indicators (who's online, identified by share link display name)
349 - - Attribution on every change ("Alice added", "Bob completed")
350 - - Assignment to collaborators (by display name)
351 - - Activity feed showing who did what
352 - - Comments on tasks (attributed to share link)
353 -
354 - ### Attribution Display
355 -
356 - Each change shows who made it:
357 - - **Owner's view**: "Alice updated task" (Alice = display_name from her share_link)
358 - - **Alice's view**: "You updated task" or "Bob updated task"
359 -
360 - The display_name is set when generating the share link:
361 - - Owner types "Alice" when creating her link
362 - - Alice sees herself as "You"
363 - - Owner and other collaborators see "Alice"
364 -
365 - ### Calendar Overlay
366 -
367 - ```
368 - ┌─────────────────────────────────────────────────────┐
369 - │ Day Plan - Tuesday, Feb 18 │
370 - ├─────────────────────────────────────────────────────┤
371 - │ 09:00 ████████ My meeting │
372 - │ 10:00 ░░░░░░░░ Alice: Busy │
373 - │ 11:00 ████████ Focus time │
374 - │ 12:00 │
375 - │ 13:00 ░░░░░░░░ Alice: Busy ░░░░ Bob: Busy │
376 - │ 14:00 ████████ Shared: Team sync │
377 - └─────────────────────────────────────────────────────┘
378 - ```
379 -
380 - - Your events: Full details
381 - - Others' events: Just busy/free (unless they share details)
382 - - Shared events: Visible to all participants
383 -
384 - ---
385 -
386 - ## Implementation Phases
387 -
388 - ### Phase 1: Foundation
389 -
390 - - [ ] User identity & key management
391 - - [ ] Generate keypair on first launch
392 - - [ ] Secure key storage (OS keychain via `keyring` crate)
393 - - [ ] Key backup/recovery flow
394 - - [ ] UUID-based share model
395 - - [ ] `shared_projects` table for projects we share/receive
396 - - [ ] `share_links` table for per-collaborator links
397 - - [ ] `my_share_links` table for tracking links we've used
398 - - [ ] `change_log` table for attributed changes
399 - - [ ] Crypto primitives
400 - - [ ] Integrate libsodium
401 - - [ ] Encrypt/decrypt helpers
402 - - [ ] Key derivation from share link URL fragment
403 -
404 - ### Phase 2: Relay Server
405 -
406 - - [ ] Simple relay server
407 - - [ ] Rust + Axum
408 - - [ ] WebSocket for real-time
409 - - [ ] Store encrypted blobs by project UUID
410 - - [ ] Validate share_link_id on sync requests
411 - - [ ] Sync protocol
412 - - [ ] Push changes with share_link_id attribution
413 - - [ ] Pull changes, decrypt, apply
414 - - [ ] Basic conflict handling (LWW + vector clocks)
415 - - [ ] Connection management in Tauri
416 - - [ ] Auto-reconnect
417 - - [ ] Offline queue (changes stored until sync)
418 -
419 - ### Phase 3: Sharing UX
420 -
421 - - [ ] Share modal UI
422 - - [ ] Show project URL (permanent)
423 - - [ ] "Generate Link" with name input and permission select
424 - - [ ] List active share links with last_seen
425 - - [ ] Revoke button per share link
426 - - [ ] Join flow
427 - - [ ] Handle `goingson://join/{uuid}` deep links
428 - - [ ] Store share_link_id in `my_share_links`
429 - - [ ] Sync project on join
430 - - [ ] Shared projects in sidebar
431 - - [ ] "Shared with me" section
432 - - [ ] Show collaborator count badge
433 -
434 - ### Phase 4: Attribution & Activity
435 -
436 - - [ ] Change attribution
437 - - [ ] Tag every change with share_link_id
438 - - [ ] Display "Alice updated task" in UI
439 - - [ ] Activity timeline per project
440 - - [ ] Presence system
441 - - [ ] Heartbeat via relay with share_link_id
442 - - [ ] Show online collaborators by display_name
443 - - [ ] Task assignment
444 - - [ ] Assign to collaborator (by share_link display_name)
445 - - [ ] Filter by assignee
446 -
447 - ### Phase 5: Real-time Features
448 -
449 - - [ ] Live updates
450 - - [ ] WebSocket push of changes
451 - - [ ] Merge incoming changes in real-time
452 - - [ ] Comments
453 - - [ ] Add comments to tasks (attributed to share_link)
454 - - [ ] Threaded replies
455 - - [ ] @mentions (by display_name)
456 -
457 - ### Phase 6: Calendar Sharing
458 -
459 - - [ ] Busy/free sharing
460 - - [ ] Generate availability overlay
461 - - [ ] Subscribe to collaborators' availability
462 - - [ ] Shared events
463 - - [ ] Create event visible to project
464 - - [ ] RSVP functionality
465 -
466 - ### Phase 7: Polish & Security
467 -
468 - - [ ] Security audit
469 - - [ ] Share link expiration enforcement
470 - - [ ] Revocation propagation (notify relay to reject)
471 - - [ ] Audit log (who did what, when)
472 - - [ ] P2P option for local network (future)
473 -
474 - ---
475 -
476 - ## Security Considerations
477 -
478 - ### Threat Model
479 -
480 - | Threat | Mitigation |
481 - |--------|------------|
482 - | Server compromise | E2EE - server never sees plaintext |
483 - | Man-in-the-middle | Public key verification, key pinning |
484 - | Key theft | Keys encrypted at rest, OS keychain |
485 - | Metadata leakage | Minimize metadata, encrypt what's possible |
486 - | Replay attacks | Nonces, timestamps, vector clocks |
487 - | Malicious collaborator | Permissions, audit log, key rotation |
488 -
489 - ### Key Ceremonies
490 -
491 - 1. **Initial Setup**: Generate keypair, backup seed phrase
492 - 2. **Device Addition**: Verify via existing device or seed phrase
493 - 3. **Collaborator Addition**: Optional key verification (show fingerprint)
494 - 4. **Key Rotation**: When collaborator removed, rotate share key
495 -
496 - ### What the Server Knows
497 -
498 - - Public keys of users
499 - - Which encrypted blobs belong to which shares
500 - - Sync timestamps
Lines truncated
@@ -1,288 +0,0 @@
1 - # GoingsOn - Build & Distribution Targets
2 -
3 - Cross-platform distribution strategy for macOS, Linux, and Windows.
4 -
5 - ---
6 -
7 - ## Goal
8 -
9 - One-liner installation via curl/shell script:
10 -
11 - ```bash
12 - # macOS/Linux
13 - curl -fsSL https://goingson.app/install.sh | sh
14 -
15 - # Windows (PowerShell)
16 - irm https://goingson.app/install.ps1 | iex
17 - ```
18 -
19 - ---
20 -
21 - ## Target Platforms
22 -
23 - ### macOS
24 -
25 - **Architectures:** x86_64 (Intel) and aarch64 (Apple Silicon)
26 -
27 - **Distribution formats:**
28 - - `.dmg` - Drag-to-Applications installer (primary)
29 - - `.app` bundle in `.tar.gz` - For script installation
30 - - Homebrew Cask (future)
31 -
32 - **Code signing:**
33 - - [ ] Apple Developer account ($99/year)
34 - - [ ] Sign with `codesign`
35 - - [ ] Notarize with `notarytool`
36 - - [ ] Staple notarization ticket
37 -
38 - **Build commands:**
39 - ```bash
40 - # Universal binary (both architectures)
41 - cargo tauri build --target universal-apple-darwin
42 -
43 - # Or individual:
44 - cargo tauri build --target x86_64-apple-darwin
45 - cargo tauri build --target aarch64-apple-darwin
46 - ```
47 -
48 - ### Linux
49 -
50 - **Architectures:** x86_64, aarch64
51 -
52 - **Distribution formats:**
53 - - `.AppImage` - Universal, no install needed (primary)
54 - - `.deb` - Debian/Ubuntu
55 - - `.rpm` - Fedora/RHEL (via alien or native build)
56 - - Flatpak (future)
57 - - Snap (future)
58 -
59 - **Dependencies:**
60 - - WebKit2GTK 4.1
61 - - OpenSSL
62 - - libayatana-appindicator or libappindicator
63 -
64 - **Build commands:**
65 - ```bash
66 - # Native build
67 - cargo tauri build
68 -
69 - # Cross-compile for ARM (requires cross-compilation setup)
70 - cargo tauri build --target aarch64-unknown-linux-gnu
71 - ```
72 -
73 - ### Windows
74 -
75 - **Architectures:** x86_64, aarch64 (ARM64 Windows)
76 -
77 - **Distribution formats:**
78 - - `.msi` - Windows Installer (primary)
79 - - `.exe` - NSIS installer
80 - - Portable `.zip` (no install)
81 - - winget (future)
82 -
83 - **Code signing:**
84 - - [ ] Code signing certificate (EV recommended to avoid SmartScreen warnings)
85 - - [ ] Sign with `signtool` or `osslsigncode`
86 -
87 - **Build commands:**
88 - ```bash
89 - cargo tauri build --target x86_64-pc-windows-msvc
90 - cargo tauri build --target aarch64-pc-windows-msvc
91 - ```
92 -
93 - ---
94 -
95 - ## Installation Script Strategy
96 -
97 - ### install.sh (macOS/Linux)
98 -
99 - ```bash
100 - #!/bin/sh
101 - set -e
102 -
103 - REPO="goingson/goingson"
104 - INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
105 -
106 - # Detect OS and architecture
107 - OS="$(uname -s)"
108 - ARCH="$(uname -m)"
109 -
110 - case "$OS" in
111 - Darwin)
112 - case "$ARCH" in
113 - x86_64) TARGET="x86_64-apple-darwin" ;;
114 - arm64) TARGET="aarch64-apple-darwin" ;;
115 - *) echo "Unsupported architecture: $ARCH"; exit 1 ;;
116 - esac
117 - ;;
118 - Linux)
119 - case "$ARCH" in
120 - x86_64) TARGET="x86_64-unknown-linux-gnu" ;;
121 - aarch64) TARGET="aarch64-unknown-linux-gnu" ;;
122 - *) echo "Unsupported architecture: $ARCH"; exit 1 ;;
123 - esac
124 - ;;
125 - *)
126 - echo "Unsupported OS: $OS"
127 - exit 1
128 - ;;
129 - esac
130 -
131 - # Get latest release URL
132 - RELEASE_URL="https://github.com/$REPO/releases/latest/download/goingson-$TARGET.tar.gz"
133 -
134 - echo "Downloading GoingsOn for $TARGET..."
135 - curl -fsSL "$RELEASE_URL" | tar -xz -C "$INSTALL_DIR"
136 -
137 - echo "GoingsOn installed to $INSTALL_DIR/goingson"
138 - echo "Run 'goingson' to start"
139 - ```
140 -
141 - ### install.ps1 (Windows)
142 -
143 - ```powershell
144 - $ErrorActionPreference = "Stop"
145 -
146 - $repo = "goingson/goingson"
147 - $arch = if ([Environment]::Is64BitOperatingSystem) { "x86_64" } else { "i686" }
148 - $target = "$arch-pc-windows-msvc"
149 - $installDir = "$env:LOCALAPPDATA\GoingsOn"
150 -
151 - $releaseUrl = "https://github.com/$repo/releases/latest/download/goingson-$target.zip"
152 -
153 - Write-Host "Downloading GoingsOn for Windows..."
154 - $tempFile = "$env:TEMP\goingson.zip"
155 - Invoke-WebRequest -Uri $releaseUrl -OutFile $tempFile
156 -
157 - Write-Host "Installing to $installDir..."
158 - Expand-Archive -Path $tempFile -DestinationPath $installDir -Force
159 - Remove-Item $tempFile
160 -
161 - # Add to PATH
162 - $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
163 - if ($userPath -notlike "*$installDir*") {
164 - [Environment]::SetEnvironmentVariable("Path", "$userPath;$installDir", "User")
165 - Write-Host "Added $installDir to PATH"
166 - }
167 -
168 - Write-Host "GoingsOn installed! Restart your terminal and run 'goingson'"
169 - ```
170 -
171 - ---
172 -
173 - ## GitHub Actions CI/CD
174 -
175 - ### Build Matrix
176 -
177 - ```yaml
178 - name: Release
179 -
180 - on:
181 - push:
182 - tags: ['v*']
183 -
184 - jobs:
185 - build:
186 - strategy:
187 - matrix:
188 - include:
189 - # macOS
190 - - os: macos-latest
191 - target: x86_64-apple-darwin
192 - - os: macos-latest
193 - target: aarch64-apple-darwin
194 - # Linux
195 - - os: ubuntu-latest
196 - target: x86_64-unknown-linux-gnu
197 - # Windows
198 - - os: windows-latest
199 - target: x86_64-pc-windows-msvc
200 -
201 - runs-on: ${{ matrix.os }}
202 -
203 - steps:
204 - - uses: actions/checkout@v4
205 -
206 - - name: Install Rust
207 - uses: dtolnay/rust-action@stable
208 - with:
209 - targets: ${{ matrix.target }}
210 -
211 - - name: Install Linux dependencies
212 - if: matrix.os == 'ubuntu-latest'
213 - run: |
214 - sudo apt-get update
215 - sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev
216 -
217 - - name: Build
218 - run: cargo tauri build --target ${{ matrix.target }}
219 -
220 - - name: Upload artifacts
221 - uses: actions/upload-artifact@v4
222 - with:
223 - name: goingson-${{ matrix.target }}
224 - path: |
225 - target/${{ matrix.target }}/release/bundle/**/*.dmg
226 - target/${{ matrix.target }}/release/bundle/**/*.AppImage
227 - target/${{ matrix.target }}/release/bundle/**/*.msi
228 - ```
229 -
230 - ---
231 -
232 - ## Release Checklist
233 -
234 - ### Pre-release
235 - - [ ] Update version in `Cargo.toml` (workspace)
236 - - [ ] Update version in `src-tauri/tauri.conf.json`
237 - - [ ] Update CHANGELOG.md
238 - - [ ] Test on all platforms locally or via CI
239 -
240 - ### Release
241 - - [ ] Create git tag: `git tag v0.1.0`
242 - - [ ] Push tag: `git push origin v0.1.0`
243 - - [ ] CI builds all targets
244 - - [ ] Verify artifacts download correctly
245 - - [ ] Update install scripts if URLs changed
246 -
247 - ### Post-release
248 - - [ ] Announce on relevant channels
249 - - [ ] Update website download links
250 - - [ ] Submit to package managers (Homebrew, winget, etc.)
251 -
252 - ---
253 -
254 - ## Package Manager Submissions (Future)
255 -
256 - ### Homebrew (macOS)
257 - ```ruby
258 - cask "goingson" do
259 - version "0.1.0"
260 - sha256 "..."
261 - url "https://github.com/goingson/goingson/releases/download/v#{version}/GoingsOn_#{version}_universal.dmg"
262 - name "GoingsOn"
263 - homepage "https://goingson.app"
264 - app "GoingsOn.app"
265 - end
266 - ```
267 -
268 - ### winget (Windows)
269 - Submit to microsoft/winget-pkgs repository.
270 -
271 - ### Flatpak (Linux)
272 - Create manifest for Flathub submission.
273 -
274 - ---
275 -
276 - ## MCP Server Distribution
277 -
278 - The MCP server (`goingson-mcp`) should also be distributed:
279 -
280 - ```bash
281 - # Standalone binary (no Tauri dependencies)
282 - cargo build -p goingson-mcp --release
283 -
284 - # Include in install script as optional component
285 - # Or separate: curl -fsSL https://goingson.app/install-mcp.sh | sh
286 - ```
287 -
288 - The MCP binary is much smaller (~7MB) than the full app and can be distributed separately for CLI/agent users who don't need the GUI.
@@ -1,329 +0,0 @@
1 - # GoingsOn - Roadmap
2 -
3 - Pending features and improvements. See `todo_done.md` for completed work.
4 -
5 - ---
6 -
7 - ## Pre-Alpha — Desktop Ready
8 -
9 - Ship a signed macOS app to friends and family for real-world testing.
10 -
11 - ### macOS Distribution
12 -
13 - Signing & notarization steps tracked in `launch.md` (Apple Developer section).
14 -
15 - - [x] Apple Developer account approved, certificate installed
16 - - [x] API Key configured for notarization (MVR74793B9)
17 - - [x] `cargo tauri build --bundles dmg` — signed, notarization submitted
18 - - [x] Tauri updater Ed25519 key pair generated (`~/.tauri/goingson.key`)
19 - - [x] Updater public key wired into `tauri.conf.json`
20 - - [x] App icon finalized (all sizes, icns, ico, tray icons)
21 - - [ ] Verify notarization completed (`xcrun notarytool history`)
22 - - [x] Feedback mechanism: testers email me@maxj.phd
23 -
24 - ---
25 -
26 - ## Post-Alpha
27 -
28 - ### Mobile Port (Tauri 2)
29 -
30 - **In progress.** CSS-first responsive design with touch gesture module. Same Rust backend, same Tauri commands, same JS modules — changes are CSS media queries + a small `touch.js` module.
31 -
32 - See **[todo_mobile.md](./todo_mobile.md)** for remaining work and checklists.
33 -
34 - **Done:**
35 - - [x] CSS foundation: `@media (max-width: 768px)` responsive breakpoint
36 - - [x] Floating nav dot (bottom-right) with radial petal dial for view switching
37 - - [x] Nav dot styled as Skeubrute button, petals as rotated rectangular spokes
38 - - [x] Auto-computed petal radius (no overlap), 90° arc based on corner position
39 - - [x] Mobile header: "Project Management" hidden, current view title shown next to logo
40 - - [x] Page titles hidden on mobile (redundant with header)
41 - - [x] Contacts & Settings in header (upper-right, stacked), not in dial
42 - - [x] Events tab pushed to far right on desktop
43 - - [x] Event status indicator below nav dot (closed) / on Events petal (open)
44 - - [x] Status indicator logic: empty if no events left today, green if none imminent
45 - - [x] Task table → card layout with priority-colored left borders
46 - - [x] Modals → bottom sheets with drag handle
47 - - [x] Action bottom sheets replace context menus on touch
48 - - [x] Touch module (`touch.js`): long-press, swipe actions, pull-to-refresh, drag-to-dismiss
49 - - [x] Day plan: tap-to-create on touch, swipe day navigation, collapsible sidebar
50 - - [x] Email compose: in-app modal on mobile (no separate window)
51 - - [x] Keyboard shortcuts disabled on touch devices
52 - - [x] `@media (hover: none)` disables sticky hover effects
53 - - [x] Safe area insets on fixed elements
54 - - [x] Cargo.toml: desktop-only deps gated with `cfg(not(mobile))`
55 - - [x] Rust: `db_watcher` and `notifications` modules gated for desktop only
56 - - [x] `tauri.conf.json`: identifier changed to `com.goingson.app`, minWidth 320
57 -
58 - **Done (build):**
59 - - [x] `cargo tauri ios init` — Xcode project generated
60 - - [x] Capabilities split: `default.json` (cross-platform) + `desktop.json` (shell, notifications)
61 - - [x] `lib.rs`: `build_mobile_app()` + `#[cfg(mobile)]` entry point
62 - - [x] `commands/window.rs`: desktop-only gating for compose window + set title
63 - - [x] `Cargo.toml`: `crate-type = ["staticlib", "cdylib", "lib"]`
64 - - [x] Builds and runs on iOS simulator (iPhone 17 Pro, iOS 26.2)
65 - - [x] Safe area inset padding on header for dynamic island
66 -
67 - **Done (interaction wiring):**
68 - - [x] Swipe actions wired to task rows (complete/snooze), email items (archive/delete), event rows (delete) — via `mobile.js` event delegation
69 - - [x] Pull-to-refresh wired to tasks, emails, events list views
70 - - [x] Long-press selection mode on task/email list items
71 - - [x] Nav dot drag-to-reposition with edge snapping
72 - - [x] Fixed synckit-client path dep (was broken, `cargo run` now works)
73 -
74 - **Remaining:**
75 - - [ ] `cargo tauri android init`
76 - - [ ] Test all CRUD operations on mobile WebView
77 - - [ ] Physical device testing and polish
78 -
79 - ### Windows Build
80 -
81 - - [ ] `cargo tauri build` on Windows — verify `.msi` installer works (`.ico` already exists)
82 - - [ ] Test on Windows (VM or physical)
83 - - [ ] Code-sign with Authenticode certificate
84 -
85 - ---
86 -
87 - ### Kanban View
88 -
89 - State-based board view for tasks. Reuses existing task states (not started, active, done, etc.) as columns. Tasks appear as cards in the column matching their state. Drag between columns to change state.
90 -
91 - - [ ] Kanban view toggle in tasks tab (list view ↔ board view)
92 - - [ ] Board columns generated from task states
93 - - [ ] Task cards with priority color, title, due date
94 - - [ ] Drag-and-drop between columns updates task state
95 - - [ ] Respect current project/filter context
96 -
97 - ### Calendar Views
98 -
99 - Rethink navigation: separate domain-object tabs (tasks, email, events, contacts) from temporal views (day, week, month). Temporal views show cross-domain content (tasks + events + time blocks) on a shared timeline.
100 -
101 - - [ ] Restructure navigation: domain tabs vs temporal views (day/week/month)
102 - - [ ] Month calendar view (grid with event + task dots, click to expand day)
103 - - [ ] Week view (multi-day timeline, events + time blocks side by side)
104 -
105 - ### Time Tracking
106 -
107 - Estimates, actuals, and focus timer as one cohesive feature. Useful for freelancers billing by the hour.
108 -
109 - - [ ] Optional `estimated_minutes` field on tasks
110 - - [ ] Display estimate in task detail and day plan
111 - - [ ] Sum estimates in day plan view (total planned time for the day)
112 - - [ ] Start/stop timer on a task (tracks actual minutes spent)
113 - - [ ] Actual vs estimated comparison in task detail
114 - - [ ] Focus timer mode: countdown (Pomodoro-style) linked to active task, minimal UI
115 - - [ ] Time tracking summary: per-project and per-day totals
116 -
117 - ### Cloud Sync
118 -
119 - Multi-device sync with pluggable storage providers.
120 -
121 - > **Backend:** MakeNotWork SyncKit (white-labeled). GoingsOn is the first app and demo product for MNW's cloud sync service. See `~/Git/makenotwork/synckit_cloud_sync_plan.md`.
122 -
123 - **Design Principles:**
124 - - **End-to-end encrypted**: All data encrypted client-side before leaving device
125 - - Offline-first: Never block or nag about offline state, queue changes silently
126 - - Field-level merge: Auto-resolve when changes don't overlap, only prompt for true conflicts
127 - - Zero-knowledge: Even GoingsOn Cloud cannot read your data
128 -
129 - #### Phase 1: Sync Core & Conflict Resolution
130 - - [ ] `SyncConflict` and `EntitySnapshot` types
131 - - [ ] `sync_conflicts` table (migration)
132 - - [ ] Auto-merge logic: if fields don't overlap, merge silently
133 - - [ ] Conflict detection on sync pull
134 - - [ ] Resolution application on user choice
135 - - [ ] `SyncProvider` trait (list, upload, download, delete, get_metadata)
136 - - [ ] Change tracking in SQLite (vector clocks per entity)
137 - - [ ] Sync state machine (idle → syncing → conflict → resolved)
138 - - [ ] Offline queue for pending changes
139 -
140 - #### Phase 2: End-to-End Encryption
141 - - [ ] E2EE layer (XChaCha20-Poly1305)
142 - - [ ] Passphrase-based key derivation (Argon2id)
143 - - [ ] OS keychain integration for key storage
144 - - [ ] Device key management (add/remove devices)
145 - - [ ] Recovery key generation (printable paper backup)
146 - - [ ] Encrypt-before-upload / decrypt-after-download hooks
147 - - [ ] Key rotation support
148 -
149 - #### Phase 3: MNW Cloud Integration
150 - GoingsOn uses MakeNotWork's SyncKit as its cloud backend (white-labeled).
151 - See ~/Git/makenotwork/synckit_cloud_sync_plan.md for server architecture.
152 -
153 - - [ ] Integrate SyncKit Rust SDK (change tracking, batch sync)
154 - - [ ] User account provisioning via MNW API
155 - - [ ] Auth token management (JWT from MNW, stored in OS keychain)
156 - - [ ] Sync settings UI (account, devices, sync frequency)
157 - - [ ] White-label branding ("GoingsOn Cloud" in-app, powered by MNW)
158 -
159 - #### Phase 4: Sync Provider Plugins
160 -
161 - **Note:** Depends on Cloud Sync Phases 1-3.
162 -
163 - **Default: GoingsOn Cloud (MNW SyncKit)** — Built-in, zero-config, white-labeled MNW backend.
164 -
165 - **Alternative providers:**
166 - - [ ] Local filesystem (for iCloud Drive, Dropbox folder, OneDrive folder)
167 - - [ ] WebDAV (Nextcloud, Fastmail, ownCloud)
168 - - [ ] Dropbox API
169 - - [ ] Google Drive API
170 - - [ ] S3-compatible (AWS S3, Backblaze B2, MinIO, Cloudflare R2)
171 - - [ ] OneDrive API
172 -
173 - #### Phase 5: Sync UX
174 - - [ ] Settings UI for provider configuration
175 - - [ ] Sync status indicator
176 - - [ ] Conflict resolution modal (side-by-side field comparison)
177 - - [ ] Sync history/log viewer
178 - - [ ] E2EE key management UI
179 -
180 - #### Phase 6: Cloud-Dependent Features
181 - - [ ] Send Later: queue outbound email with send-at timestamp (requires server relay or IMAP drafts scheduling)
182 - - [ ] Activity log: audit trail of task/project/event changes (stored locally, synced via cloud)
183 -
184 - ---
185 -
186 - ### File Attachments
187 -
188 - **Note:** Depends on Cloud Sync for cloud-compatible storage.
189 -
190 - - [ ] Attachment storage abstraction (uses same `SyncProvider` trait)
191 - - [ ] Attach files to tasks and projects
192 - - [ ] View email attachments inline
193 - - [ ] Download attachments
194 - - [ ] Link to local files (non-synced reference)
195 - - [ ] Thumbnail generation for images
196 -
197 - ---
198 -
199 - ### Passkey Authentication (WebAuthn/FIDO2)
200 -
201 - #### Phase 1: Local Biometric Unlock
202 - - [ ] Optional app lock requiring biometric/PIN
203 - - [ ] Platform authenticator integration (Touch ID, Windows Hello, libfido2)
204 - - [ ] Lock timeout settings
205 - - [ ] Fallback to master password
206 -
207 - #### Phase 2: Hardware Security Key Support
208 - - [ ] FIDO2/WebAuthn credential registration
209 - - [ ] Support for YubiKey, SoloKey, etc.
210 - - [ ] Key management UI
211 -
212 - #### Phase 3: Cloud Passkey Authentication
213 - - [ ] Passkey registration with GoingsOn Cloud
214 - - [ ] Cross-device passkey sync
215 -
216 - #### Phase 4: E2EE Key Protection
217 - - [ ] Derive encryption key from passkey PRF extension
218 - - [ ] Hardware-bound encryption keys
219 -
220 - ---
221 -
222 - ### Agent Integration (MCP Server) — Remaining Phases
223 -
224 - **Phase 3: In-App Agent Tab** (requires LLM configured)
225 - - [ ] Agent tab UI with chat interface
226 - - [ ] Message history persistence
227 - - [ ] Connect to existing LLM service
228 - - [ ] Tool calling support (reuse MCP tool definitions)
229 - - [ ] Action confirmation dialogs
230 - - [ ] Streaming responses via Tauri events
231 -
232 - **Phase 4: Polish**
233 - - [ ] Conversation persistence
234 - - [ ] Suggested actions
235 - - [ ] Keyboard shortcuts for agent
236 -
237 - ### Plugin System (Rhai) — Remaining Phases
238 -
239 - #### Phase 3: Additional Plugin Types
240 - - [ ] Export adapters
241 - - [ ] Custom commands
242 - - [ ] Lifecycle hooks (on_task_created, on_email_received, etc.)
243 -
244 - #### Phase 4: Hot-Reload & Distribution
245 - - [ ] File watcher for .rhai changes
246 - - [ ] AST cache for performance
247 - - [ ] Install from URL/file
248 - - [ ] Update checking
249 -
250 - #### Starter Plugins
251 -
252 - **Import:**
253 - - [ ] TaskWarrior JSON import
254 - - [ ] Todoist API import
255 - - [ ] Things 3 import (macOS)
256 - - [ ] Apple Reminders import (macOS)
257 - - [ ] Notion database import
258 - - [ ] Trello board import
259 - - [ ] Google Tasks import
260 -
261 - **Export:**
262 - - [ ] CSV export (configurable columns)
263 - - [ ] JSON export (full data dump)
264 - - [ ] Markdown export (task lists)
265 - - [ ] ICS calendar export (enhanced)
266 - - [ ] Obsidian markdown export
267 -
268 - #### Contact Plugins
269 -
270 - **Import:**
271 - - [ ] vCard (.vcf) import/export
272 - - [ ] Apple Contacts import
273 - - [ ] Google Contacts CSV/JSON import
274 - - [ ] Microsoft Outlook CSV import
275 - - [ ] LinkedIn connections import
276 -
277 - **Sync (two-way):**
278 - - [ ] CardDAV sync (Fastmail, iCloud, Nextcloud)
279 - - [ ] Google Contacts API sync
280 - - [ ] Microsoft Graph contacts sync
281 -
282 - #### Calendar Sync Plugins
283 - - [ ] Google Calendar API (OAuth2 + REST API)
284 - - [ ] Apple Calendar / iCloud CalDAV
285 - - [ ] CalDAV generic (Fastmail, Nextcloud, etc.)
286 - - [ ] Two-way event sync
287 - - [ ] Import .ics files
288 -
289 - #### External Tools
290 - - [ ] Raycast extension
291 - - [ ] Alfred workflow
292 - - [ ] iOS Shortcuts integration
293 - - [ ] Zapier/Make webhooks
294 -
295 - ---
296 -
297 - ## Architecture
298 -
299 - ```
300 - workspace/
301 - ├── crates/
302 - │ ├── core/ # Domain types, urgency calc, parser, repository traits
303 - │ ├── db-sqlite/ # SQLite repository implementations
304 - │ └── plugin-runtime/ # Rhai plugin system
305 - ├── plugins/ # Bundled reference plugins
306 - ├── src-tauri/ # Desktop app (Tauri 2 + vanilla JS)
307 - └── migrations/
308 - └── sqlite/ # SQLite migrations
309 - ```
310 -
311 - **Stack:** Rust, Tokio, SQLx, Tauri 2, SQLite, Vanilla JS
312 -
313 - **Run:** `cargo run` launches the desktop app.
314 -
315 - ---
316 -
317 - ## Notes
318 -
319 - ## Deferred
320 - - [ ] Portability: publish synckit-client as a crate or use git submodule (path dep fixed to ../../../synckit-client but still requires sibling repo)
321 - - [ ] Apple Watch app: quick task capture + glanceable agenda (separate Swift micro-app, Tauri doesn't target watchOS)
322 - - [ ] Home screen widgets (iOS/Android): today's tasks, upcoming events (requires native WidgetKit / App Widgets per platform)
323 -
324 - ---
325 -
326 - - **Domain purchased:** goingson.app (Feb 2026)
327 - - docs/ARCHITECTURE.md documents crate structure and data flow
328 - - OAuth setup: See `launch.md` for provider registration
329 - - MCP testing: See `docs/MCP_test.md` for integration test instructions
@@ -1,747 +0,0 @@
1 - # GoingsOn - Completed Features
2 -
3 - A productivity app for independent workers managing projects, tasks, emails, and calendar events.
4 -
5 - ---
6 -
7 - ## Core Features (Implemented)
8 -
9 - ### Projects
10 - - [x] Full CRUD operations
11 - - [x] 7 project types: Job, SideProject, Company, Essay, Article, Painting, Other
12 - - [x] Status tracking: Active, OnHold, Completed, Archived
13 - - [x] Project dashboard with linked tasks/events/emails
14 -
15 - ### Tasks (TaskWarrior-inspired)
16 - - [x] Full CRUD with statuses: Pending, Started, Completed, Deleted
17 - - [x] Priority levels: High (H), Medium (M), Low (L)
18 - - [x] Tags system for flexible categorization
19 - - [x] Recurrence: Daily, Weekly, Monthly (auto-creates next instance on completion)
20 - - [x] Subtasks (ordered checkbox items within tasks)
21 - - [x] Annotations (timestamped notes/comments)
22 - - [x] Due date tracking with smart date parsing
23 - - [x] Urgency calculation algorithm:
24 - - Priority coefficient (H=6.0, M=3.9, L=1.8)
25 - - Overdue penalty (+12.0)
26 - - Due soon scaling (within 7 days)
27 - - Task age bonus (up to 2.0 over 30 days)
28 - - Started status bonus (+4.0)
29 - - "Urgent" tag bonus (+2.0)
30 - - [x] Quick-add parser with natural language syntax:
31 - - `+tag` for tags
32 - - `project:Name` or `proj:Name` for project linking
33 - - `priority:H/M/L` or `pri:high/medium/low`
34 - - `due:tomorrow`, `due:2026-02-15`, `due:monday`, `due:+3d`
35 - - `recur:daily|weekly|monthly`
36 -
37 - ### Events
38 - - [x] Full CRUD for calendar events
39 - - [x] Title, description, location, time range
40 - - [x] Event recurrence (Daily, Weekly, Monthly)
41 - - [x] Task-event linking (auto-create events from task due dates)
42 - - [x] Upcoming events view
43 - - [x] Date badge proximity coloring (today=green, tomorrow=yellow, this week=cyan, future=blue, past=gray)
44 -
45 - ### Emails
46 - - [x] Email storage and inbox management
47 - - [x] Archive system
48 - - [x] Read/unread status tracking
49 - - [x] Email-to-project linking
50 - - [x] Email-to-task conversion with source tracking
51 - - [x] Unread count endpoint
52 - - [x] Outgoing email flag for sent messages
53 -
54 - ### Workflow Features
55 - - [x] Snooze/defer tasks and emails until specified date
56 - - [x] Waiting-for-response tracking with expected response dates
57 - - [x] Full-text search (SQLite FTS5)
58 - - [x] Time blocking schema (scheduled_start + duration fields)
59 -
60 - ### Weekly Review (2026-02-14)
61 - - [x] Full tab view for guided weekly review workflow
62 - - [x] Past week section with stats (completed tasks, overdue tasks, events, pending count)
63 - - [x] Coming week section with stats (upcoming events, tasks due, already overdue)
64 - - [x] Task focus system for weekly priorities:
65 - - Toggle focus on individual tasks (star icon)
66 - - Clear all focus option
67 - - Focused projects derived from focused tasks
68 - - Available-for-focus list (high priority, not snoozed/waiting)
69 - - [x] Completion tracking with notes textarea
70 - - [x] Monday nudge mechanism (tab badge + startup toast)
71 - - [x] All computation in Rust backend (no JS business logic):
72 - - Pre-computed counts, formatted dates, derived projects
73 - - Frontend only renders the response
74 -
75 - **Database:**
76 - - Migration `018_weekly_review.sql`: `weekly_reviews` table + `is_focus`/`focus_set_at` task columns
77 -
78 - **Backend:**
79 - - `crates/core/src/models.rs`: `WeeklyReview` struct, `Task.is_focus`/`focus_set_at` fields
80 - - `crates/core/src/repository.rs`: `WeeklyReviewRepository` trait, `TaskRepository` focus methods
81 - - `crates/db-sqlite/src/repository/weekly_review_repo.rs`: SQLite implementation
82 - - `src-tauri/src/commands/weekly_review.rs`: 5 commands returning pre-computed `WeeklyReviewResponse`
83 -
84 - **Frontend:**
85 - - `js/weekly-review.js`: Minimal IIFE module (render only, no computation)
86 - - `index.html`: Tab + view container
87 - - `api.js`: `weeklyReview` namespace
88 - - `styles.css`: Stat cards, sections, focus toggles, review status badges
89 -
90 - ### Authentication
91 - - [x] Desktop single-user mode (fixed desktop@localhost user)
92 - - [x] Argon2 password hashing (for future use)
93 -
94 - ### Dashboard & Statistics
95 - - [x] Overdue task count
96 - - [x] Tasks due today
97 - - [x] Tasks due this week
98 - - [x] Unread email count
99 - - [x] Upcoming events
100 - - [x] Active projects count
101 -
102 - ---
103 -
104 - ## Email Integration (IMAP/SMTP)
105 - - [x] IMAP client for fetching emails (async_imap + tokio_native_tls)
106 - - [x] SMTP client for sending emails (lettre)
107 - - [x] Email account configuration management
108 - - [x] IMAP UID-based deduplication
109 - - [x] Folder tracking for archive management
110 - - [x] Gmail support (imap.gmail.com:993, smtp.gmail.com:587)
111 - - [x] Fastmail support (archive folder detection)
112 - - [x] Generic IMAP/SMTP provider support
113 -
114 - ---
115 -
116 - ## OAuth2 + JMAP Integration
117 -
118 - ### OAuth2 Infrastructure
119 - - [x] Generic `OAuthProvider` trait supporting multiple providers
120 - - [x] PKCE (Proof Key for Code Exchange) for secure desktop app flow
121 - - [x] Local HTTP callback server on `127.0.0.1:{random_port}`
122 - - [x] Polling endpoint for frontend to detect callback
123 - - [x] `TokenManager` for token refresh lifecycle
124 - - [x] `EmailAuthType` enum: `Password`, `OAuth2Fastmail`, `OAuth2Google`, `OAuth2Microsoft`, `OAuth2Yahoo`
125 -
126 - ### Provider Implementations
127 - - [x] Fastmail OAuth2 + JMAP (full email sync via JMAP protocol)
128 - - [x] Google OAuth2 provider + XOAUTH2 IMAP/SMTP
129 - - [x] Microsoft OAuth2 provider + XOAUTH2 IMAP/SMTP
130 - - [x] Yahoo OAuth2 provider + XOAUTH2 IMAP/SMTP
131 -
132 - ### XOAUTH2 Authentication
133 - - [x] `ImapClient::with_oauth()` - IMAP connection with XOAUTH2
134 - - [x] `SmtpClient::with_oauth()` - SMTP connection with XOAUTH2 mechanism
135 - - [x] `ImapAuth` enum for password vs OAuth authentication
136 - - [x] `SmtpAuth` enum for password vs OAuth authentication
137 - - [x] Token refresh before IMAP/SMTP operations
138 - - [x] Updated sync, test, send, archive, unarchive to use XOAUTH2 for OAuth accounts
139 -
140 - ### JMAP Client
141 - - [x] Session discovery and caching
142 - - [x] Email fetch (inbox + archive)
143 - - [x] Email send via JMAP Submission
144 - - [x] Mailbox listing and operations
145 - - [x] Archive/unarchive via mailbox moves
146 -
147 - ### Database Schema
148 - - [x] Migration 017: OAuth columns (`auth_type`, `oauth2_access_token`, `oauth2_refresh_token`, `oauth2_token_expires_at`, `jmap_session_url`, `jmap_account_id`)
149 - - [x] Repository methods for OAuth account creation and token updates
150 -
151 - ### Frontend
152 - - [x] OAuth provider buttons in "Add Account" modal
153 - - [x] OAuth badges on account list (Fastmail, Google, etc.)
154 - - [x] Reconnect button for OAuth accounts (vs Edit for password)
155 - - [x] OAuth flow UI with waiting modal and callback detection
156 -
157 - ---
158 -
159 - ## LLM Templates
160 - - [x] `{: prompt :}` syntax for **dynamic** templates (re-evaluated at display time)
161 - - [x] Ollama / OpenAI-compatible provider support
162 - - [x] Context injection (day_of_year, date, etc.)
163 - - [x] Response caching with date-based invalidation
164 -
165 - ---
166 -
167 - ## Desktop UX (Tauri-native)
168 - - [x] Native menu bar (File/Edit/View/Tools/Help) with keyboard shortcuts
169 - - File: New Task (Cmd+N), New Project (Cmd+Shift+N), Save View (Cmd+S), Close
170 - - Edit: Undo, Redo, Cut, Copy, Paste, Select All
171 - - View: Projects (Cmd+1), Tasks (Cmd+2), Events (Cmd+3), Emails (Cmd+4), Day Plan (Cmd+5), Toggle Sidebar
172 - - Tools: Sync Email (Cmd+Shift+E), Settings (Cmd+,)
173 - - Help: Keyboard Shortcuts (?), About
174 - - [x] Context menus on items (right-click tasks/emails/events/projects)
175 - - [x] Dynamic window title (reflect current view)
176 - - [x] Visible focus states (accessibility)
177 - - [x] Persist window size/position between sessions
178 - - [x] Theme system with 10 themes (Neobrute, Catppuccin variants, Dracula, Nord, Tokyo Night, Flatwhite, Ayu Light)
179 - - [x] System notifications via `tauri-plugin-notification`
180 - - Background snooze watcher (60s interval)
181 - - Task snooze expiry notifications
182 - - Email snooze expiry notifications
183 - - Overdue waiting response reminders
184 - - Deduplication to avoid repeat notifications
185 - - [x] Dark mode from system preference (Follow System option)
186 - - [x] Theme persistence via localStorage
187 - - [x] `/convert-helix-theme` skill for converting Helix editor themes
188 - - [x] Day Plan: Removed quick-time buttons (Morning/Noon/Evening/Now)
189 - - [x] Standard OS keyboard shortcuts (Cmd+Q quit on macOS, File > Exit on Windows/Linux)
190 - - [x] Email reader: Large modal (nearly full-screen), improved readability
191 - - [x] Email reader: Actions dropdown (Convert to Task, Convert to Event)
192 - - [x] Email reader: Reader mode automatically strips HTML for clean display
193 - - [x] Email reader: "Open in Browser" for original HTML view
194 - - [x] Email IMAP sync: Properly syncs read/unread status from server
195 - - [x] Email auto-sync: Configurable per-account interval (5/15/30/60 min), background scheduler
196 - - [x] Task page: Fixed column alignment and overflow/ellipsis handling
197 - - [x] Task page: Fixed priority column sorting (H/M/L value mapping)
198 - - [x] Fixed button shadow clipping (sharp corners) in Tasks, Events, Emails, Day Plan views
199 -
200 - ---
201 -
202 - ## JS Architecture Cleanup (2026-02-15)
203 -
204 - **Goal:** Remove `window.*` exports, migrate to `GoingsOn.*` namespace, move computation to Rust.
205 -
206 - **Namespace migration:**
207 - - [x] Deleted `saved-views.js` (418 lines)
208 - - [x] Moved snooze calculations to Rust backend with unit tests
209 - - [x] Rewrote `day-planning.js` with IIFE (29 → 2 exports)
210 - - [x] Rewrote `bulk-actions.js` with IIFE
211 - - [x] Rewrote `snooze.js` with IIFE
212 - - [x] Added `GoingsOn.ui` namespace (removed 16 exports)
213 - - [x] Added `GoingsOn.settings` namespace (removed 6 exports)
214 - - [x] Updated HTML onclick handlers to use namespaces
215 - - [x] `utils.js` → `GoingsOn.utils` (removed 20 window exports)
216 - - [x] `navigation.js` → `GoingsOn.navigation` (removed 5 window exports)
217 - - [x] `themes.js` → `GoingsOn.themes` (IIFE wrapped, removed 10 exports)
218 - - [x] `keyboard.js` → `GoingsOn.keyboard` (IIFE wrapped, removed 19 exports)
219 - - [x] `context-menus.js` → `GoingsOn.contextMenus` (IIFE wrapped, removed 4 exports)
220 - - [x] Updated `components.js`, `settings.js`, `app.js` to use namespace utilities
221 - - [x] Updated domain modules to use `GoingsOn.contextMenus.*` for context menu handlers
222 - - [x] Removed backward-compat window aliases from domain modules: `tasks.js` (~31), `emails.js` (~28), `projects.js` (~20), `events.js` (~8), `day-planning.js` (2)
223 - - [x] Migrated all cross-file bare function calls to `GoingsOn.*` namespace
224 - - [x] Removed dead frontend search API definition from `api.js`
225 - - [x] Migrated `components.js` core aliases to `GoingsOn.ui.*`
226 - - [x] Converted `settings.js` to IIFE module with `GoingsOn.settings` namespace
227 - - [x] Added `GoingsOn.app` namespace (`toggleSidebar`, `syncAllEmailAccounts`, `openAboutModal`)
228 - - [x] Removed all remaining window aliases
229 - - [x] Migrated `window.api` → `GoingsOn.api`, `window.AppState` → `GoingsOn.state`
230 - - [x] Migrated all managers and utilities to `GoingsOn.*` namespace
231 -
232 - **Rust pre-computed fields:**
233 - - [x] `TaskResponse`: `subtaskProgress`, `dueFormatted`, `urgencyClass`, `isOverdue`, `isSnoozed`
234 - - [x] `EventResponse`: `timeFormatted`, `dateFormatted`, `isPast`, `proximityClass`, `proximityLabel`
235 - - [x] `EmailResponse`: `receivedFormatted`
236 - - [x] `EmailAccountResponse`: `lastSyncFormatted`
237 - - [x] Backend pre-sorts events (ASC), past events (DESC), project tasks (by urgency), email threads (by date)
238 -
239 - **State & dead code cleanup:**
240 - - [x] Migrated module-local caches to `GoingsOn.state` (events, emails)
241 - - [x] Removed 7 dead utility functions (~90 lines)
242 - - [x] Updated `ARCHITECTURE.md` with frontend architecture section
243 -
244 - ---
245 -
246 - ## Performance Optimization (2026-02-15)
247 -
248 - - [x] **Virtual scrolling for large lists**
249 - - [x] Created `VirtualScroller` component (`js/virtual-scroller.js`)
250 - - [x] Task list uses virtual scrolling (replaces pagination)
251 - - [x] Events list uses virtual scrolling
252 - - [x] Email list virtual scrolling
253 - - [x] Day Plan unscheduled tasks sidebar virtual scrolling
254 -
255 - ---
256 -
257 - ## Weekly Review Redesign (2026-02-15)
258 -
259 - **Mockup:** `mockups/weekly-review.html`
260 -
261 - #### Phase 1: Backend Data Extensions
262 - - [x] Extended `WeeklyReviewResponse` with `timelineDays`, `carriedOverTasks`, `projectHealth`
263 - - [x] Compute timeline dot data (completed, events, overdue counts per day)
264 -
265 - #### Phase 2: CSS Grid Layout
266 - - [x] `.review-grid`, `.review-card`, `.week-timeline`, `.stat-box`, `.focus-slot`, `.project-health`
267 - - [x] Responsive breakpoints at 900px and 600px
268 -
269 - #### Phase 3: JavaScript Rewrite
270 - - [x] Rewrote `weekly-review.js` render function to match mockup structure
271 - - [x] Focus slot assignment via suggested task buttons
272 - - [x] Auto-save draft on input change (debounced)
273 -
274 - #### Phase 4: Polish
275 - - [x] Smooth transitions when assigning focus
276 - - [x] Keyboard navigation for focus slots
277 - - [x] Print styles for weekly review
278 - - [x] "Share as image" export option
279 -
280 - ---
281 -
282 - ## Export/Backup (2026-02-15)
283 -
284 - - [x] Export all data as JSON
285 - - [x] Export tasks as CSV
286 - - [x] Export calendar as ICS
287 - - [x] Create/restore/delete backups
288 - - [x] Automated backup schedule (daily compressed backups with configurable retention)
289 -
290 - ---
291 -
292 - ## Contacts (2026-02-15)
293 -
294 - #### Phase 1: Core Infrastructure
295 - - [x] `Contact` model in `crates/core/src/contact.rs` (with ContactEmail, ContactPhone, SocialHandle sub-entities)
296 - - [x] `ContactRepository` trait with CRUD + sub-collection management (14 methods)
297 - - [x] SQLite implementation with migration (`023_contacts.sql`) — 4 tables + FTS5 + triggers
298 - - [x] Tauri commands: `list_contacts`, `get_contact`, `create_contact`, `update_contact`, `delete_contact` + 6 sub-collection commands
299 - - [x] Full-text search across name, nickname, company, notes, tags
300 - - [x] Search integration (contacts appear in global search results)
301 -
302 - #### Phase 2: Basic UI
303 - - [x] Contacts list view (card grid with search and tag filter)
304 - - [x] Contact detail view (modal with sub-collection lists)
305 - - [x] Create/edit contact modal (using openFormModal)
306 - - [x] Social handle input with platform field
307 - - [x] Tags management (create, assign, filter by tag)
308 - - [x] Contacts tab in navigation (Cmd+5), router integration
309 - - [x] Initials avatar (colored circle with display initials)
310 -
311 - #### Phase 2.5: Custom Fields
312 - - [x] Arbitrary custom fields on contacts (label + optional URL + display value)
313 - - Migration: `024_contact_custom_fields.sql`
314 - - Core type: `ContactCustomField` / `NewContactCustomField`
315 - - Repository: `add_custom_field` / `remove_custom_field` methods
316 - - Tauri commands: `add_contact_custom_field` / `remove_contact_custom_field`
317 - - Frontend: add/remove UI in contact detail modal, values linkable via URL
318 -
319 - #### Phase 3: Integration with Existing Features
320 - - [x] Link contacts to tasks (contact_id FK, select dropdown in task form, badge in task list)
321 - - [x] Link contacts to events (contact_id FK, select dropdown in event form)
322 - - [x] Auto-suggest contact from email sender (find_contact_by_email lookup in email reader)
323 - - [x] Create contact from email address ("+ Save Contact" button, pre-fills name/email)
324 - - [x] Show contact card in email thread view (initials avatar, name, company, "View Contact")
325 - - [x] Auto-link contact when creating task/event from email (sender email lookup)
326 -
327 - **Files:**
328 - - `crates/core/src/contact.rs` - Contact model and types
329 - - `crates/db-sqlite/src/repository/contact_repo.rs` - SQLite implementation
330 - - `src-tauri/src/commands/contact.rs` - Tauri commands (14 commands, incl. `find_contact_by_email`)
331 - - `src-tauri/frontend/js/contacts.js` - Frontend module (IIFE, card grid, detail modal)
332 - - `migrations/sqlite/023_contacts.sql` - Database schema (4 tables + FTS5)
333 - - `migrations/sqlite/024_contact_custom_fields.sql` - Custom fields table
334 - - `migrations/sqlite/025_contact_linking.sql` - contact_id FK on tasks + events
335 -
336 - ---
337 -
338 - ## Agent Integration (MCP Server) — Phases 1-2 (2026-02-15)
339 -
340 - **Phase 1: MCP Server (Basic)**
341 - - [x] Create `goingson-mcp` crate with rmcp
342 - - [x] Implement core tools: `list_tasks`, `create_task`, `complete_task`, `list_projects`
343 - - [x] SQLite connection to same DB as Tauri app (`~/.config/goingson/goingson.db`)
344 - - [x] Claude Code integration (`~/.claude/mcp.json`)
345 -
346 - **Phase 1b: MCP Server (Extended)**
347 - - [x] Additional task tools: `update_task`, `delete_task`, `snooze_task`
348 - - [x] Subtask linking: `add_subtask_link` (link tasks as subtasks of other tasks)
349 - - [x] Utility tools: `search`, `get_context`, `export_roadmap`
350 - - [x] Project tools: `create_project`
351 -
352 - **Phase 1c: MCP Server (Full Management)**
353 - - [x] All project CRUD: `update_project`, `delete_project`, `get_project`
354 - - [x] All task tools: `get_task`, `start_task`, snooze/unsnooze, waiting, annotations, subtasks (16 tools)
355 - - [x] All event tools: `create_event`, `list_events`, `list_upcoming_events`, `get_event`, `update_event`, `delete_event`
356 - - [x] Dashboard stats: `get_dashboard_stats`
357 -
358 - **Phase 2: GUI Change Detection**
359 - - [x] DB file watcher (`db_watcher.rs` using `notify` crate)
360 - - [x] Refresh views on external changes (emits `db:external-change` event)
361 - - [x] Frontend listens and refreshes current view (skips if modal open)
362 -
363 - ---
364 -
365 - ## Plugin System (Rhai) — Phases 1-2 (2026-02-15)
366 -
367 - #### Phase 1: Core Infrastructure
368 - - [x] Create `plugin-runtime` crate with Rhai engine
369 - - [x] Plugin manifest format (`plugin.toml`)
370 - - [x] Plugin loader with safety limits
371 - - [x] Basic `goingson::` API module (read_file, parse_csv, parse_json, logging)
372 - - [x] Tauri commands for plugin management
373 -
374 - #### Phase 2: Import Plugins
375 - - [x] Import plugin trait and execution
376 - - [x] CSV import reference plugin
377 - - [x] Import preview UI (file selector + parsed data table)
378 - - [x] Import execution UI (progress + result summary)
379 - - [x] UI: Plugin manager (enable/disable plugins)
380 -
381 - ---
382 -
383 - ## Feature Roadmap - Completed Tiers
384 -
385 - ### Tier 1 - Daily Workflow Essentials
386 -
387 - #### Search
388 - - [x] Full-text search across emails, tasks, projects, events
389 - - [x] Search by type filter
390 - - [x] Search by project association
391 - - [x] Search by date range (supports after:/before:/from:/to: syntax)
392 - - [x] **Search bar removed from header** (2026-02-15) — Full-text search still available via MCP and backend.
393 -
394 - #### Keyboard Shortcuts
395 - - [x] Global shortcut overlay (press `?` to show)
396 - - [x] Navigation: `g t` go to tasks, `g e` go to emails, `g p` go to projects
397 - - [x] Actions: `a` archive, `c` complete, `n` new item
398 - - [x] Quick add: `q` open quick-add from anywhere
399 - - [x] List navigation: `j`/`k` up/down, `Enter` to open
400 -
401 - #### Snooze/Defer
402 - - [x] Snooze emails until specific date/time
403 - - [x] Defer tasks (hide from active views until date)
404 - - [x] List snoozed items API endpoint
405 - - [x] Quick snooze options UI: later today, tomorrow, next week, custom
406 - - [x] Snoozed items indicator (badge on tasks/emails)
407 - - [x] Auto-resurface notification when snooze expires (Tauri native notifications)
408 -
409 - #### Email-to-Task Quick Conversion
410 - - [x] One-click "Create task from email" button
411 - - [x] Auto-link task to source email
412 - - [x] Pull subject as task title (editable)
413 - - [x] Option to include email body as task notes
414 - - [x] Keyboard shortcut `t` on email to create task
415 -
416 - #### Task-Event Integration
417 - - [x] Tasks with due dates auto-create linked calendar events
418 - - [x] Event reflects task due date/time
419 - - [x] Completing task updates/removes linked event
420 - - [x] Editing task due date updates linked event
421 - - [x] Events can recur (daily, weekly, monthly)
422 - - [x] Tasks can recur (daily, weekly, monthly)
423 -
424 - ### Tier 2 - Triage at Scale
425 -
426 - #### Bulk Actions
427 - - [x] Multi-select with checkboxes
428 - - [x] Shift-click range selection
429 - - [x] Select all in current view
430 - - [x] Bulk archive, delete, snooze (tasks and emails)
431 -
432 - #### Filters & Saved Views
433 - - [x] Filter by project, tag, date range, status
434 - - [x] Combine multiple filters (AND logic)
435 - - [x] Save filter as named view (backend ready)
436 - - [x] Quick access to saved views in sidebar
437 - - [x] Pinned views in left sidebar
438 - - [x] Load/apply saved view on click
439 - - [x] Manage views modal (rename, unpin, delete)
440 -
441 - #### Follow-up Tracking
442 - - [x] Mark email/task as "waiting for response"
443 - - [x] Set expected response date
444 - - [x] Waiting-for list view
445 - - [x] Reminder when response overdue (Tauri notifications)
446 - - [x] Auto-clear waiting status when reply received
447 -
448 - #### Email Threading
449 - - [x] Group emails by conversation thread
450 - - [x] Thread count badge in email list
451 - - [x] Chronological thread display (forum-style, oldest first)
452 - - [x] Jump to related emails from task
453 -
454 - ### Tier 3 - Time & Planning
455 -
456 - #### Time Blocking
457 - - [x] Schema: scheduled_start + scheduled_duration fields
458 - - [x] UI for assigning time blocks to tasks
459 - - [x] Day view showing blocked vs available time (Day Plan view)
460 - - [x] Conflict detection with existing events
461 - - [x] Time block types: Free Time, Personal, Vacation, Focus (`block_type` column on events)
462 - - [x] Day Plan timeline renders blocks with distinct colors (cyan/yellow/purple/red)
463 - - [x] Paint-to-create modal supports Event / Time Block / Link to Task modes
464 - - [x] Block type select in Events form (create/edit)
465 -
466 - #### Email Reader Mode
467 - - [x] Convert HTML emails to clean reader format
468 - - [x] Strip formatting cruft, extract readable text
469 - - [x] Never render raw HTML (security)
470 -
471 - ---
472 -
473 - ## Layer 1: Core Local Features (2026-02-16)
474 -
475 - ### Vacation Day Toggles
476 - - [x] Migration `027_vacation_days.sql`: `vacation_days` TEXT column on `weekly_reviews`
477 - - [x] Core model: `vacation_days: Vec<u8>` on `WeeklyReview` (0=Mon, 6=Sun)
478 - - [x] Repository: parse/serialize comma-separated day indices, `set_vacation_days` method
479 - - [x] Tauri command: `set_vacation_days`, `is_vacation` field on timeline days
480 - - [x] Day Planning: `is_vacation_day` field on `DayPlanningResponse`, "Day Off" banner
481 - - [x] Frontend: MTWTFSS toggle buttons in weekly review, dimmed timeline days, vacation dot indicator
482 - - [x] CSS: vacation toggles, timeline dimming, day plan banner
483 -
484 - ### LLM AI-Fill Button
485 - - [x] Static template detection: `(: prompt :)` syntax (distinct from dynamic `{: prompt :}`)
486 - - [x] `hasStaticTemplates()`, `extractStaticPrompts()`, `expandStaticTemplates()` in `llm-templates.js`
487 - - [x] AI-Fill button on form modal text/textarea fields when `(: ... :)` detected
488 - - [x] Click fills field with LLM output, button becomes "Regenerate"
489 - - [x] Gated on `GoingsOn.llmTemplates.isEnabled()` (no button if LLM not configured)
490 -
491 - ### Project Milestones — Phase 1
492 - - [x] Migration `028_milestones.sql`: `milestones` table + `milestone_id` FK on tasks
493 - - [x] Core models: `Milestone`, `NewMilestone`, `MilestoneStatus` (Open/Completed)
494 - - [x] `milestone_id` added to `Task`, `NewTask`, `UpdateTask`, `NewTaskBuilder`
495 - - [x] `MilestoneRepository` trait (list_by_project, get_by_id, create, update, delete, reorder)
496 - - [x] SQLite implementation (`milestone_repo.rs`)
497 - - [x] Task repo updated with `milestone_id` across SELECT, create, update, complete
498 - - [x] Tauri commands: `list_milestones`, `create_milestone`, `update_milestone`, `delete_milestone`, `reorder_milestones`
499 - - [x] `MilestoneResponse` with pre-computed `task_count`, `completed_count`, `progress`
500 - - [x] `TaskResponse` includes `milestone_id`
Lines truncated