Skip to main content

max / goingson

Embed SyncKit API key, remove key entry UI, fix OAuth flow - Add synckit.toml with embedded API key (public client ID, not a secret) - Remove API key entry UI from settings-sync.js (collapse to OAuth button) - Remove sync_test_api_key/sync_save_api_key commands - Add sync_get_tiers command for server-driven pricing - Fix sync_status: read authenticated from in-memory session, not keychain (keychain read was returning None, causing auth state to never persist) - Fix OAuth callback server: add CORS header for tauri://localhost origin - Fix OAuth token exchange: send form-urlencoded (not JSON) per OAuth spec - Fix poll loop: skip pending responses instead of stopping on first 200 - Revert CSP to null (was blocking inline handlers since 46401c2) - Update todo: mark sync indicator and annual messaging as done Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-11 18:04 UTC
Commit: 722a07dfa29967c734d40e12b21c0c1ca84527b5
Parent: 39fb977
9 files changed, +201 insertions, -157 deletions
M docs/todo/todo.md +86 -40
@@ -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"