max / goingson
- Co-Authored-By
- Claude Opus 4.8 <noreply@anthropic.com>
15 files changed,
+650 insertions,
-37 deletions
| @@ -39,17 +39,22 @@ | |||
| 39 | 39 | /// time_sessions, milestones, daily_notes, attachments, and sync_accounts). | |
| 40 | 40 | pub(crate) async fn collect_full_export( | |
| 41 | 41 | state: &AppState, | |
| 42 | + | user_id: goingson_core::UserId, | |
| 42 | 43 | ) -> Result<FullExport, goingson_core::CoreError> { | |
| 43 | - | let projects = state.projects.list_all(DESKTOP_USER_ID).await?; | |
| 44 | - | let tasks = state.tasks.list_all(DESKTOP_USER_ID).await?; | |
| 45 | - | let events = state.events.list_all(DESKTOP_USER_ID).await?; | |
| 46 | - | let emails = state.emails.list_all(DESKTOP_USER_ID, true).await?; | |
| 47 | - | let contacts = state.contacts.list_all(DESKTOP_USER_ID).await?; | |
| 48 | - | let time_sessions = state.tasks.list_all_time_sessions(DESKTOP_USER_ID).await?; | |
| 49 | - | let milestones = state.milestones.list_all(DESKTOP_USER_ID).await?; | |
| 50 | - | let daily_notes = state.daily_notes.list_all(DESKTOP_USER_ID).await?; | |
| 51 | - | let attachments = state.attachments.list_all(DESKTOP_USER_ID).await?; | |
| 52 | - | let sync_accounts = state.sync_accounts.list_all(DESKTOP_USER_ID).await?; | |
| 44 | + | let projects = state.projects.list_all(user_id).await?; | |
| 45 | + | let tasks = state.tasks.list_all(user_id).await?; | |
| 46 | + | let events = state.events.list_all(user_id).await?; | |
| 47 | + | let emails = state.emails.list_all(user_id, true).await?; | |
| 48 | + | let contacts = state.contacts.list_all(user_id).await?; | |
| 49 | + | let time_sessions = state.tasks.list_all_time_sessions(user_id).await?; | |
| 50 | + | let milestones = state.milestones.list_all(user_id).await?; | |
| 51 | + | let daily_notes = state.daily_notes.list_all(user_id).await?; | |
| 52 | + | let attachments = state.attachments.list_all(user_id).await?; | |
| 53 | + | let sync_accounts = state.sync_accounts.list_all(user_id).await?; | |
| 54 | + | let saved_views = state.saved_views.list_all(user_id).await?; | |
| 55 | + | let weekly_reviews = state.weekly_reviews.list_all(user_id).await?; | |
| 56 | + | let monthly_goals = state.monthly_reviews.list_all_goals(user_id).await?; | |
| 57 | + | let monthly_reflections = state.monthly_reviews.list_all_reflections(user_id).await?; | |
| 53 | 58 | Ok(FullExport::new( | |
| 54 | 59 | projects, | |
| 55 | 60 | tasks, | |
| @@ -61,6 +66,10 @@ | |||
| 61 | 66 | daily_notes, | |
| 62 | 67 | attachments, | |
| 63 | 68 | sync_accounts, | |
| 69 | + | saved_views, | |
| 70 | + | weekly_reviews, | |
| 71 | + | monthly_goals, | |
| 72 | + | monthly_reflections, | |
| 64 | 73 | )) | |
| 65 | 74 | } | |
| 66 | 75 | ||
| @@ -155,7 +164,7 @@ | |||
| 155 | 164 | let filename = backup_filename(now); | |
| 156 | 165 | let file_path = backup_dir.join(&filename); | |
| 157 | 166 | ||
| 158 | - | let export = collect_full_export(state).await.map_err(|e| e.to_string())?; | |
| 167 | + | let export = collect_full_export(state, DESKTOP_USER_ID).await.map_err(|e| e.to_string())?; | |
| 159 | 168 | let item_count = export.total_count(); | |
| 160 | 169 | let max_to_keep = settings.max_backups_to_keep as usize; | |
| 161 | 170 | ||
| @@ -253,7 +262,7 @@ | |||
| 253 | 262 | let filename = backup_filename(now); | |
| 254 | 263 | let file_path = backup_dir.join(&filename); | |
| 255 | 264 | ||
| 256 | - | let export = collect_full_export(state).await.map_err(|e| e.to_string())?; | |
| 265 | + | let export = collect_full_export(state, DESKTOP_USER_ID).await.map_err(|e| e.to_string())?; | |
| 257 | 266 | let item_count = export.total_count(); | |
| 258 | 267 | let size_bytes = write_backup(&export, &file_path).map_err(|e| format!("Failed to write backup: {}", e))?; | |
| 259 | 268 |
| @@ -6,7 +6,10 @@ | |||
| 6 | 6 | //! and sub-collections are written verbatim; this module only defines the types that | |
| 7 | 7 | //! cross the command -> storage boundary. | |
| 8 | 8 | ||
| 9 | - | use crate::models::{Attachment, DailyNote, Email, Event, Milestone, Project, SyncAccount, Task, TimeSession}; | |
| 9 | + | use crate::models::{ | |
| 10 | + | Attachment, DailyNote, Email, Event, Milestone, MonthlyGoal, MonthlyReflection, Project, | |
| 11 | + | SavedView, SyncAccount, Task, TimeSession, WeeklyReview, | |
| 12 | + | }; | |
| 10 | 13 | use crate::Contact; | |
| 11 | 14 | ||
| 12 | 15 | /// Result of a restore operation. | |
| @@ -36,6 +39,14 @@ | |||
| 36 | 39 | pub attachments_restored: usize, | |
| 37 | 40 | /// Number of sync accounts restored. | |
| 38 | 41 | pub sync_accounts_restored: usize, | |
| 42 | + | /// Number of saved views restored. | |
| 43 | + | pub saved_views_restored: usize, | |
| 44 | + | /// Number of weekly reviews restored. | |
| 45 | + | pub weekly_reviews_restored: usize, | |
| 46 | + | /// Number of monthly goals restored. | |
| 47 | + | pub monthly_goals_restored: usize, | |
| 48 | + | /// Number of monthly reflections restored. | |
| 49 | + | pub monthly_reflections_restored: usize, | |
| 39 | 50 | } | |
| 40 | 51 | ||
| 41 | 52 | /// Pre-parsed backup data for restoration. | |
| @@ -50,6 +61,10 @@ | |||
| 50 | 61 | pub daily_notes: Vec<DailyNote>, | |
| 51 | 62 | pub attachments: Vec<Attachment>, | |
| 52 | 63 | pub sync_accounts: Vec<SyncAccount>, | |
| 64 | + | pub saved_views: Vec<SavedView>, | |
| 65 | + | pub weekly_reviews: Vec<WeeklyReview>, | |
| 66 | + | pub monthly_goals: Vec<MonthlyGoal>, | |
| 67 | + | pub monthly_reflections: Vec<MonthlyReflection>, | |
| 53 | 68 | } | |
| 54 | 69 | ||
| 55 | 70 | #[cfg(test)] | |
| @@ -78,6 +93,10 @@ | |||
| 78 | 93 | daily_notes: vec![], | |
| 79 | 94 | attachments: vec![], | |
| 80 | 95 | sync_accounts: vec![], | |
| 96 | + | saved_views: vec![], | |
| 97 | + | weekly_reviews: vec![], | |
| 98 | + | monthly_goals: vec![], | |
| 99 | + | monthly_reflections: vec![], | |
| 81 | 100 | }; | |
| 82 | 101 | assert!(input.projects.is_empty()); | |
| 83 | 102 | assert!(input.tasks.is_empty()); |
| @@ -809,6 +809,9 @@ | |||
| 809 | 809 | /// Creates or updates a weekly review. | |
| 810 | 810 | async fn upsert(&self, user_id: UserId, week_start: NaiveDate, notes: &str) -> Result<crate::models::WeeklyReview>; | |
| 811 | 811 | ||
| 812 | + | /// Lists every weekly review for a user across all weeks (for full backup export). | |
| 813 | + | async fn list_all(&self, user_id: UserId) -> Result<Vec<crate::models::WeeklyReview>>; | |
| 814 | + | ||
| 812 | 815 | /// Checks if the current week's review is completed. | |
| 813 | 816 | async fn is_current_week_completed(&self, user_id: UserId) -> Result<bool>; | |
| 814 | 817 | ||
| @@ -835,6 +838,12 @@ | |||
| 835 | 838 | /// Gets all goals for a specific month. | |
| 836 | 839 | async fn list_goals(&self, user_id: UserId, month: &str) -> Result<Vec<crate::models::MonthlyGoal>>; | |
| 837 | 840 | ||
| 841 | + | /// Lists every goal for a user across all months (for full backup export). | |
| 842 | + | async fn list_all_goals(&self, user_id: UserId) -> Result<Vec<crate::models::MonthlyGoal>>; | |
| 843 | + | ||
| 844 | + | /// Lists every reflection for a user across all months (for full backup export). | |
| 845 | + | async fn list_all_reflections(&self, user_id: UserId) -> Result<Vec<crate::models::MonthlyReflection>>; | |
| 846 | + | ||
| 838 | 847 | /// Creates or updates a goal for a month at a given position (1-3). | |
| 839 | 848 | async fn upsert_goal(&self, user_id: UserId, month: &str, text: &str, position: i32) -> Result<crate::models::MonthlyGoal>; | |
| 840 | 849 |
| @@ -72,4 +72,4 @@ | |||
| 72 | 72 | SqliteWeeklyReviewRepository, | |
| 73 | 73 | }; | |
| 74 | 74 | ||
| 75 | - | pub use repository::restore::restore_all; | |
| 75 | + | pub use repository::restore::{restore_all, BACKUP_TABLES, EXCLUDED_TABLES}; |
| @@ -75,6 +75,10 @@ | |||
| 75 | 75 | daily_notes: vec![], | |
| 76 | 76 | attachments: vec![], | |
| 77 | 77 | sync_accounts: vec![], | |
| 78 | + | saved_views: vec![], | |
| 79 | + | weekly_reviews: vec![], | |
| 80 | + | monthly_goals: vec![], | |
| 81 | + | monthly_reflections: vec![], | |
| 78 | 82 | }; | |
| 79 | 83 | ||
| 80 | 84 | // First restore recreates with status + completed_at + id preserved. | |
| @@ -181,6 +185,10 @@ | |||
| 181 | 185 | daily_notes: vec![], | |
| 182 | 186 | attachments: vec![], | |
| 183 | 187 | sync_accounts: vec![], | |
| 188 | + | saved_views: vec![], | |
| 189 | + | weekly_reviews: vec![], | |
| 190 | + | monthly_goals: vec![], | |
| 191 | + | monthly_reflections: vec![], | |
| 184 | 192 | }; | |
| 185 | 193 | let r = restore_all(&pool, user_id, &input).await.unwrap(); | |
| 186 | 194 | assert_eq!(r.tasks_restored, 1); | |
| @@ -274,6 +282,10 @@ | |||
| 274 | 282 | daily_notes: vec![], | |
| 275 | 283 | attachments: vec![], | |
| 276 | 284 | sync_accounts: vec![], | |
| 285 | + | saved_views: vec![], | |
| 286 | + | weekly_reviews: vec![], | |
| 287 | + | monthly_goals: vec![], | |
| 288 | + | monthly_reflections: vec![], | |
| 277 | 289 | }; | |
| 278 | 290 | let r = restore_all(&pool, user_id, &input).await.unwrap(); | |
| 279 | 291 | assert_eq!(r.events_restored, 1); | |
| @@ -385,6 +397,10 @@ | |||
| 385 | 397 | daily_notes: vec![note.clone()], | |
| 386 | 398 | attachments: vec![attachment.clone()], | |
| 387 | 399 | sync_accounts: vec![account.clone()], | |
| 400 | + | saved_views: vec![], | |
| 401 | + | weekly_reviews: vec![], | |
| 402 | + | monthly_goals: vec![], | |
| 403 | + | monthly_reflections: vec![], | |
| 388 | 404 | }; | |
| 389 | 405 | // Deleting tasks cascades time_sessions + attachments; deleting projects | |
| 390 | 406 | // cascades milestones; daily_notes and sync_accounts are independent. |
| @@ -139,7 +139,7 @@ | |||
| 139 | 139 | ) -> Result<ExportResponse, ApiError> { | |
| 140 | 140 | validate_export_path(&file_path)?; | |
| 141 | 141 | ||
| 142 | - | let export = crate::backup_scheduler::collect_full_export(&state).await?; | |
| 142 | + | let export = crate::backup_scheduler::collect_full_export(&state, DESKTOP_USER_ID).await?; | |
| 143 | 143 | let item_count = export.total_count(); | |
| 144 | 144 | ||
| 145 | 145 | let size_bytes = backup::write_json(&export, &file_path) | |
| @@ -260,7 +260,7 @@ | |||
| 260 | 260 | let filename = format!("goingson-backup-{}.json.gz", timestamp); | |
| 261 | 261 | let file_path = backup_dir.join(&filename); | |
| 262 | 262 | ||
| 263 | - | let export = crate::backup_scheduler::collect_full_export(&state).await?; | |
| 263 | + | let export = crate::backup_scheduler::collect_full_export(&state, DESKTOP_USER_ID).await?; | |
| 264 | 264 | let item_count = export.total_count(); | |
| 265 | 265 | ||
| 266 | 266 | let size_bytes = backup::write_backup(&export, &file_path) | |
| @@ -380,6 +380,10 @@ | |||
| 380 | 380 | daily_notes: export.daily_notes, | |
| 381 | 381 | attachments: export.attachments, | |
| 382 | 382 | sync_accounts: export.sync_accounts, | |
| 383 | + | saved_views: export.saved_views, | |
| 384 | + | weekly_reviews: export.weekly_reviews, | |
| 385 | + | monthly_goals: export.monthly_goals, | |
| 386 | + | monthly_reflections: export.monthly_reflections, | |
| 383 | 387 | }; | |
| 384 | 388 | ||
| 385 | 389 | // Single all-or-nothing transaction over the shared pool: a mid-restore failure |