Skip to main content

max / goingson

config: move settings off localStorage onto the shared config store Step 6 (final) of the portable-config migration. GoingsOn kept its ~9 settings in the frontend's localStorage, unsynced. They now live in a backend user_config table (migration 060) with a sync posture, using the real synckit-client config adapter — GO is on the SyncStore engine, so unlike audiofiles/BB it wires config_sync_table into goingson_schema and the engine generates the export/import triggers, gated on the config_key_policy allowlist. The engine applies the posture predicate symmetrically, so the import boundary is enforced for free. Backend: - config_key::CONFIG declares posture per key. Synced: theme, event_lead_minutes, plan/review nudges, work start/end hour. Local (per-device): ui_mode, welcomed, hint_shortcuts. Undeclared keys fail closed. - config_sync_table(&CONFIG) added to the manifest; seed_config_policy seeds config_key_policy from the spec every launch (before any migration_sql regen); run_config_migration regenerates triggers on an already-cutover device. - get_config/set_config/delete_config/get_all_config commands (sqlx over the pool; set validates keys against the spec). user_config excluded from local backup (synced keys recover via sync, device-local keys re-derive). Frontend: - New config.js: a superset drop-in for the old localStorage calls. Known keys (by clean or legacy name) read from a startup-preloaded cache and write through to the backend; other keys pass through to localStorage. ui_mode is mirrored back to localStorage so the pre-paint bootstrap-uimode.js still reads it synchronously (no flash). - One-time migration copies existing localStorage values into the backend on first load. All read/write sites (themes, settings, plan-review, events, app, viewport, components' setLocalStorage) routed through GoingsOn.config; init-time readers await config.ready. 582 backend tests pass; JS suite 56/56. The compose-page.js frontend-lint finding is pre-existing (unrelated).
Author: Max Johnson <me@maxj.phd> · 2026-07-24 23:21 UTC
Signed with PGP, not checked
Commit: b8e66fb992d46cae7788a8f3040e9f1166dd15aa
Parent: f7131f5
21 files changed, +483 insertions, -30 deletions
M Cargo.lock +10
@@ -2289,6 +2289,7 @@
2289 2289 "sha2 0.11.0",
2290 2290 "sqlx",
2291 2291 "synckit-client",
2292 + "synckit-config",
2292 2293 "tauri",
2293 2294 "tauri-build",
2294 2295 "tauri-plugin-dialog",
@@ -6193,6 +6194,7 @@
6193 6194 "serde",
6194 6195 "serde_json",
6195 6196 "sha2 0.11.0",
6197 + "synckit-config",
6196 6198 "thiserror 2.0.18",
6197 6199 "tokio",
6198 6200 "tokio-stream",
@@ -6204,6 +6206,14 @@
6204 6206 "zeroize",
6205 6207 ]
6206 6208
6209 + [[package]]
6210 + name = "synckit-config"
6211 + version = "0.1.2"
6212 + dependencies = [
6213 + "rusqlite",
6214 + "thiserror 2.0.18",
6215 + ]
6216 +
6207 6217 [[package]]
6208 6218 name = "synstructure"
6209 6219 version = "0.13.2"
@@ -20,6 +20,7 @@
20 20 goingson-core = { workspace = true }
21 21 goingson-db-sqlite = { workspace = true }
22 22 synckit-client = { path = "../../../synckit/synckit-client" }
23 + synckit-config = { path = "../../../synckit/synckit-config" }
23 24
24 25 # Tauri
25 26 tauri = { workspace = true, features = ["image-png"] }
@@ -59,4 +59,5 @@
59 59 056 30e200908dd4f6d2ebdf5b8f1419207d9c9af430f99df471b61cfd3c9659851a0f8a9b9596aeb682c6567af76bd39ff6
60 60 057 f43f541166c9beb552e22bc5170fcb7bb95db580a6f82fc5dca88e74edeb433ff22e0bd72ddf3913800075a53d360978
61 61 058 8c800b52c85af7f107d7706f4d5c3dfcc3a44f5877a606da23bb3e1df0fa4812f34f2fc78999b16bcab9a6491ff78512
62 - 059 a493b5976c6e2af49213f9d2e2ec1fb052d88e150b98a4dcaea34539472bb92ebc06ed2d66fb69738e522f208efeedd9
62 + 059 32140c4ee5bb75b9d6530a0b7c60cef872acc5fa50bb969eb9368036131d4f84931cddbcf6f8b41038fb0d5fc175af7b
63 + 060 e5b833cff6c768710b0daf88fc9cf087938278ec158c99c0789242878832e802ae00e82bef2cc45efe21644de8d9fa57
@@ -614,6 +614,7 @@
614 614
615 615 <!-- Namespace (must load first) -->
616 616 <script src="js/goingson.js"></script>
617 + <script src="js/config.js"></script>
617 618 <script src="js/dispatch.js"></script>
618 619 <script src="js/viewport.js"></script>
619 620
@@ -7,6 +7,7 @@
7 7 pub mod backup_scheduler;
8 8 pub mod blob_gc;
9 9 pub mod commands;
10 + pub mod config_key;
10 11 pub mod email;
11 12 pub mod email_sync_scheduler;
12 13 pub mod export;
@@ -52,6 +53,11 @@
52 53 $crate::commands::open_email_blob,
53 54 $crate::commands::save_email_blob,
54 55 $crate::commands::get_file_size,
56 + // Config (user_config key/value)
57 + $crate::commands::get_config,
58 + $crate::commands::get_all_config,
59 + $crate::commands::set_config,
60 + $crate::commands::delete_config,
55 61 // Projects
56 62 $crate::commands::list_projects,
57 63 $crate::commands::get_project,
@@ -149,6 +149,12 @@
149 149 let backup_settings = Arc::new(SqliteBackupSettingsRepository::new(pool.clone()));
150 150 let sync_accounts = Arc::new(SqliteSyncAccountRepository::new(pool.clone()));
151 151
152 + // Config (step 6): seed config_key_policy from the config spec every
153 + // launch, before any engine trigger regeneration below joins it. Idempotent.
154 + if let Err(e) = crate::syncstore::seed_config_policy(&db_path) {
155 + warn!("config policy seed failed (will retry next launch): {e}");
156 + }
157 +
152 158 // Groups p4 / M2: migrate this device's sync bookkeeping to the SyncStore
153 159 // engine (idempotent, guarded by the `syncstore_migrated` flag). Non-fatal
154 160 // and transactional; a failure leaves the legacy state intact and retries
@@ -164,6 +170,13 @@
164 170 warn!("SyncStore group migration failed (will retry next launch): {e}");
165 171 }
166 172
173 + // Config (step 6): regenerate triggers so an already-migrated device gains
174 + // the user_config config triggers. No-op on a fresh install (the cutover
175 + // already generated them) and once applied. Non-fatal.
176 + if let Err(e) = crate::syncstore::run_config_migration(&db_path) {
177 + warn!("SyncStore config migration failed (will retry next launch): {e}");
178 + }
179 +
167 180 // Build the SyncStore over goingson.db (its own WAL rusqlite connection,
168 181 // coexisting with the sqlx pool) when a sync client is configured.
169 182 let sync_store = load_sync_client(&app_data_dir)
@@ -71,10 +71,12 @@
71 71 GoingsOn.tasks.load();
72 72 }
73 73
74 - // First-run welcome
75 - if (!localStorage.getItem('go-welcomed')) {
74 + // First-run welcome. These flags live in the config store now; wait for the
75 + // cache so a first run is not misread as a returning one.
76 + await GoingsOn.config.ready;
77 + if (!GoingsOn.config.get('go-welcomed')) {
76 78 showWelcome();
77 - } else if (!localStorage.getItem('go-hint-shortcuts')) {
79 + } else if (!GoingsOn.config.get('go-hint-shortcuts')) {
78 80 // One-time hint after first session
79 81 setTimeout(() => showHint('go-hint-shortcuts', 'Press ? anytime to see keyboard shortcuts'), 2000);
80 82 }
@@ -376,11 +378,11 @@
376 378 }
377 379
378 380 /**
379 - * Show a one-time dismissible hint toast. Sets localStorage key so it only shows once.
381 + * Show a one-time dismissible hint toast. Sets a config flag so it only shows once.
380 382 */
381 383 function showHint(storageKey, message) {
382 - if (localStorage.getItem(storageKey)) return;
383 - localStorage.setItem(storageKey, '1');
384 + if (GoingsOn.config.get(storageKey)) return;
385 + GoingsOn.config.set(storageKey, '1');
384 386 GoingsOn.ui.showToast(message, 'info', { duration: 5000 });
385 387 }
386 388
@@ -56,9 +56,11 @@
56 56 if (on && off) el.textContent = el.classList.contains('expanded') ? on : off;
57 57 }
58 58
59 - /** Set a localStorage key (replaces inline `localStorage.setItem(...)`). */
59 + /** Set a config value. Routes through `GoingsOn.config`, so a known config key
60 + * (e.g. the nudge prefs and the welcome/hint flags) lands in the backend
61 + * `user_config` store while any other key passes through to localStorage. */
60 62 function setLocalStorage(key, value) {
61 - try { localStorage.setItem(key, value); } catch (e) { /* storage disabled */ }
63 + GoingsOn.config.set(key, value);
62 64 }
63 65
64 66 /** Run a `GoingsOn` action only when Enter was pressed (for keydown handlers). */