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).
21 files changed,
+483 insertions,
-30 deletions
| 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 |
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 |
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 |
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 |
59 |
|
056 30e200908dd4f6d2ebdf5b8f1419207d9c9af430f99df471b61cfd3c9659851a0f8a9b9596aeb682c6567af76bd39ff6
|
| 60 |
60 |
|
057 f43f541166c9beb552e22bc5170fcb7bb95db580a6f82fc5dca88e74edeb433ff22e0bd72ddf3913800075a53d360978
|
| 61 |
61 |
|
058 8c800b52c85af7f107d7706f4d5c3dfcc3a44f5877a606da23bb3e1df0fa4812f34f2fc78999b16bcab9a6491ff78512
|
| 62 |
|
- |
059 a493b5976c6e2af49213f9d2e2ec1fb052d88e150b98a4dcaea34539472bb92ebc06ed2d66fb69738e522f208efeedd9
|
|
62 |
+ |
059 32140c4ee5bb75b9d6530a0b7c60cef872acc5fa50bb969eb9368036131d4f84931cddbcf6f8b41038fb0d5fc175af7b
|
|
63 |
+ |
060 e5b833cff6c768710b0daf88fc9cf087938278ec158c99c0789242878832e802ae00e82bef2cc45efe21644de8d9fa57
|
| 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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). */
|