max / goingson
57 files changed,
+5090 insertions,
-4898 deletions
| @@ -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", |
| @@ -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">×</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
| @@ -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