fix(pres_mgmt): config sync round 2 — save race, dead fields, launcher gate, version stamps

Four fixes found while tracing why Manager-saved Config page changes
(QR, POC column, etc.) weren't reliably reaching pres_mgmt_loc:

1. Config page save was a race, not deterministic. The save handler only
   called load_ae_obj_id__event() (SWR — returns stale Dexie cache
   immediately, refreshes in the background, not awaited) and assumed
   that "picked up the new config." It never called
   sync_config__event_pres_mgmt() itself. Now calls it directly with the
   just-saved draft, so the editing browser updates instantly with no
   race. Kept the load_ae_obj_id__event() call (default try_cache: true)
   for propagating to other browsers/tabs via Dexie — do not pass
   try_cache: false there, that skips the Dexie write entirely.

2. Removed the dead "Lock Config" Sync/Unlink toggle in the sign-in
   panel (e_app_access_type.svelte). It wrote to four fields
   ($ae_loc.lock_config/sync_local_config,
   pres_mgmt_loc.current.lock_config/sync_local_config) that are never
   read anywhere (confirmed via full-repo grep), and confusingly shared
   a name with the real, functional "Lock Config" checkbox on the Pres
   Mgmt Config page. Removed the button and the now-orphaned
   lock_config/sync_local_config fields from PresMgmtLocState.

3. show__launcher_link was never assigned by sync_config__event_pres_mgmt()
   — only its inverse hide__launcher_link was. The toggle button's
   `show__launcher_link || trusted_access` visibility gate (in 3 menu
   files) always collapsed to trusted-only, ignoring the admin's setting.
   Added the missing assignment.

4. AE_PRES_MGMT_LOC_VERSION was bumped to 2 this morning claiming it
   "forces a localStorage reset" — it didn't, because _check_and_wipe()
   was never wired up for ae_pres_mgmt_loc, and even if it had been, the
   store never wrote a __version field to compare. Fixed: the store's
   serializer now stamps __version, and store_versions.ts wires the
   check. Found and fixed the same bug already live in ae_leads_loc,
   except worse there — it was wiping leads users' local prefs on EVERY
   page load, not just once.

All logged in PROJECT__AE_Events_PressMgmt_Config_Cleanup.md.
svelte-check: 0 errors, 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-16 13:21:04 -04:00
parent 8eb9444edf
commit e5c141e765
8 changed files with 120 additions and 70 deletions

View File

@@ -214,15 +214,33 @@ Safe and backward compatible — old DB records fall through to `?? false` defau
### Regression Fixes Needed (2026-06-12 Audit) ### Regression Fixes Needed (2026-06-12 Audit)
- [ ] **Add `show__launcher_link_legacy` to `PressMgmtRemoteCfg`** or remove entirely if deprecated - [x] **`hide__launcher_link_legacy` removed entirely** (other agent's "config schema cleanup
- Currently hard-coded to `true` in sync function (line 1054 `ae_events__event.ts`) phase 2" commit, 2026-06-16) — Flask launcher is fully retired, no longer hard-coded or
- Can't be controlled via config UI present anywhere in `PressMgmtRemoteCfg` / `PresMgmtLocState` / the sync function.
- [ ] **Resolve `hide__launcher_link*` local/remote conflict** - [x] **`hide__launcher_link*` / `show__launcher_link` local/remote conflict resolved
- Menu toggles ([ae_comp__events_menu_opts.svelte](../src/routes/events/ae_comp__events_menu_opts.svelte) lines 462-494) use `hide__launcher_link` for LOCAL UI state (2026-06-16)** — kept separate (they serve different purposes: `hide__launcher_link`
- Remote schema uses `show__launcher_link` (inverted) gates the launcher link *content*, `show__launcher_link` gates the manual toggle
- Decision: Keep separate? Document clearly? Unify? *button*'s visibility), but `show__launcher_link` was never actually assigned by
- [ ] **Add `AE_PRES_MGMT_LOC_VERSION` to `store_versions.ts`** (Step 2 requirement) `sync_config__event_pres_mgmt()` — only its inverse `hide__launcher_link` was. So the
- [ ] **Clean `hide__launcher_link*` from defaults** if truly deprecated (lines 154-155, 333-334 in `pres_mgmt_defaults.ts`) toggle button's `show__launcher_link || trusted_access` gate (in
`ae_comp__events_menu_opts.svelte`, `event_page_menu.svelte`,
`location_page_menu.svelte`) always collapsed to trusted-only, ignoring the admin's
setting. Added the missing `loc.show__launcher_link = ...` assignment right next to
`hide__launcher_link` in the lock-synced block.
- [x] **`AE_PRES_MGMT_LOC_VERSION` properly wired into `store_versions.ts` (2026-06-16)**
— the other agent's commit bumped this constant to 2 claiming it "forces a localStorage
reset," but `_check_and_wipe()` was never actually called for `ae_pres_mgmt_loc`, and
even if it had been, the store's serializer never wrote a `__version` field for it to
compare against — so the bump was a complete no-op. Fixed: `ae_events_stores__pres_mgmt.svelte.ts`'s
custom serializer now stamps `__version` on every write, and `store_versions.ts` calls
`_check_and_wipe('ae_pres_mgmt_loc', AE_PRES_MGMT_LOC_VERSION)`. Side effect: every
browser's existing `ae_pres_mgmt_loc` (no `__version` ever written before) will wipe
once on next load and resync clean from the remote config — this is expected and fine.
**Found the same bug already live in `ae_leads_loc`** (actively wiping leads users' local
prefs on *every* page load, not just once) and fixed it the same way — see
`ae_events_stores__leads.svelte.ts`. `badges_loc`/`launcher_loc`/`events_auth_loc` have
version constants declared but not wired into `_check_and_wipe()` at all (dormant, not
actively harmful) — not fixed, flagged for whoever picks that up next.
- [x] **POC column local/remote conflict fixed (2026-06-16)**`show__session_li_poc_field` was - [x] **POC column local/remote conflict fixed (2026-06-16)**`show__session_li_poc_field` was
local-only (never synced) and the session-list-table prop computation ignored the admin's local-only (never synced) and the session-list-table prop computation ignored the admin's
`hide__session_poc` master switch entirely. Fixed: added `show__session_li_poc_field` to `hide__session_poc` master switch entirely. Fixed: added `show__session_li_poc_field` to
@@ -259,6 +277,34 @@ Safe and backward compatible — old DB records fall through to `?? false` defau
`locations/+page.svelte`, `location/[event_location_id]/+page.svelte`, and `locations/+page.svelte`, `location/[event_location_id]/+page.svelte`, and
`reports/+page.svelte`. Any new pres_mgmt page that can be a first-load entry point `reports/+page.svelte`. Any new pres_mgmt page that can be a first-load entry point
(i.e. not always reached via `/pres_mgmt` or `/session/[id]` first) needs this same block. (i.e. not always reached via `/pres_mgmt` or `/session/[id]` first) needs this same block.
- [x] **Config page save was a race, not deterministic (2026-06-16)** — after PATCHing
`mod_pres_mgmt_json`, the save handler only called `load_ae_obj_id__event()` (SWR —
returns the stale Dexie cache immediately, refreshes from the API in the background,
*not awaited*) and assumed that "picked up the new config." It never actually called
`sync_config__event_pres_mgmt()` itself. Whether the editor's own browser reflected the
change depended entirely on winning a race against an un-awaited background fetch —
explains why specific just-changed fields (QR, POC column, profile-pic visibility, one
report key) intermittently looked stale even to the admin who just saved them, while
older unchanged fields stayed correct. Fixed: the save handler now calls
`sync_config__event_pres_mgmt({ pres_mgmt_cfg_remote: draft })` directly with the
just-saved draft, so the editing browser updates instantly with no race. (Kept the
`load_ae_obj_id__event()` call too, with its default `try_cache: true` — that's what
propagates the fresh record to Dexie for *other* browsers/tabs. Do not pass
`try_cache: false` there — that skips the Dexie write entirely, see the documented
"try_cache: false Bug" in `GUIDE__SvelteKit2_Svelte5_DexieJS.md`.)
- [x] **Removed dead "Lock Config" Sync/Unlink toggle (2026-06-16)** — a Manager-only
button in the sign-in panel (`e_app_access_type.svelte`) wrote to
`$ae_loc.lock_config`/`sync_local_config` and `pres_mgmt_loc.current.lock_config`/
`sync_local_config`. Confirmed via full-repo grep that none of those four fields are
read anywhere. It also confusingly shared the name "Lock Config" with the real,
functional checkbox on the Pres Mgmt Config page (`draft.lock_config`, part of
`PressMgmtRemoteCfg`, which actually gates `sync_config__event_pres_mgmt()`'s
remote→local sync). Removed the button and the now-fully-orphaned
`lock_config`/`sync_local_config` fields from `PresMgmtLocState`. Left
`$ae_loc.lock_config`/`sync_local_config` (the general app store) alone — `lock_config`
was never even in `ae_loc`'s declared defaults (a phantom field created ad-hoc by the
dead button), and `ae_loc.sync_local_config` is out of scope for a pres_mgmt-only pass;
defer to the planned `ae_loc` migration in `PROJECT__Stores_Svelte5_Migration.md`.
### Step 6 scope (mechanical find-replace) ### Step 6 scope (mechanical find-replace)

View File

@@ -1060,6 +1060,12 @@ export function sync_config__event_pres_mgmt({
// Launcher links (show__ in remote → invert to hide__ in local display state) // Launcher links (show__ in remote → invert to hide__ in local display state)
loc.hide__launcher_link = loc.hide__launcher_link =
!(pres_mgmt_cfg_remote?.show__launcher_link ?? false); !(pres_mgmt_cfg_remote?.show__launcher_link ?? false);
// Mirror the raw remote flag too — the toggle BUTTON's own visibility (not the
// launcher link content itself) is gated on show__launcher_link directly in
// ae_comp__events_menu_opts.svelte / event_page_menu.svelte / location_page_menu.svelte.
// This was never assigned before, so that gate always collapsed to trusted_access-only.
loc.show__launcher_link =
pres_mgmt_cfg_remote?.show__launcher_link ?? false;
// Navigation / UI constraints // Navigation / UI constraints
loc.limit__navigation = loc.limit__navigation =

View File

@@ -8,12 +8,10 @@ import { afterNavigate } from '$app/navigation';
import { import {
Lock, Lock,
LockOpen, LockOpen,
RefreshCw,
ShieldEllipsis, ShieldEllipsis,
ShieldMinus, ShieldMinus,
ShieldPlus, ShieldPlus,
ShieldUser, ShieldUser,
Unlink,
User, User,
UserCheck, UserCheck,
UserRound, UserRound,
@@ -29,8 +27,6 @@ import {
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
// import { core_func } from '$lib/ae_core/ae_core_functions'; // import { core_func } from '$lib/ae_core/ae_core_functions';
// Ideally the Event related stores should not be imported here?
import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte';
// import { db_events } from "$lib/db_events"; // import { db_events } from "$lib/db_events";
// export let hidden: boolean = false; // export let hidden: boolean = false;
@@ -361,47 +357,14 @@ function handle_clear_access() {
<div class="transition-all"> <div class="transition-all">
{#if $ae_loc.trusted_access && $ae_loc.edit_mode} {#if $ae_loc.trusted_access && $ae_loc.edit_mode}
{#if $ae_loc.manager_access} <!-- Removed 2026-06-16: dead "Lock Config" Sync/Unlink toggle. It wrote to
{#if $ae_loc?.sync_local_config} $ae_loc.sync_local_config/lock_config and pres_mgmt_loc.current.sync_local_config/
<button lock_config, but nothing in the codebase ever read any of those four fields —
type="button" confirmed via full-repo grep. It also confusingly shared a name with the real,
onclick={() => { functional "Lock Config" checkbox on the Pres Mgmt Config page (which gates
$ae_loc.sync_local_config = false; sync_config__event_pres_mgmt()'s remote->local sync via the value actually
pres_mgmt_loc.current.sync_local_config = false; stored in event.mod_pres_mgmt_json.lock_config, not this local mirror).
See PROJECT__AE_Events_PressMgmt_Config_Cleanup.md. -->
$ae_loc.lock_config = false;
pres_mgmt_loc.current.lock_config = false;
// dispatch_sync_local_config_changed();
// tick();
return false;
}}
class="btn btn-sm preset-tonal-success border-success-500 hover:preset-filled-success-500 border transition-all hover:transition-all *:hover:inline"
title="Syncing the local configuration with the remote configuration.">
<RefreshCw size="1em" class="m-1" />
<span class="hidden"> Sync </span>
</button>
{:else}
<button
type="button"
onclick={() => {
$ae_loc.sync_local_config = true;
pres_mgmt_loc.current.sync_local_config = true;
$ae_loc.lock_config = true;
pres_mgmt_loc.current.lock_config = true;
// dispatch_sync_local_config_changed();
// tick();
return true;
}}
class="btn btn-sm preset-tonal-warning border-warning-500 hover:preset-filled-warning-500 border transition-all hover:transition-all *:hover:inline"
title="Currently not syncing with the remote server. Re-sync the local configuration with the remote configuration?">
<Unlink size="1em" class="m-1" />
<span class="hidden"> Re-sync? </span>
</button>
{/if}
{/if}
<!-- {#if $ae_loc.edit_mode} <!-- {#if $ae_loc.edit_mode}
<button <button

View File

@@ -17,10 +17,22 @@
*/ */
import { PersistedState } from 'runed'; import { PersistedState } from 'runed';
import { leads_loc_defaults } from './ae_events_stores__leads_defaults'; import { leads_loc_defaults } from './ae_events_stores__leads_defaults';
import { AE_LEADS_LOC_VERSION } from './store_versions';
export const leads_loc = new PersistedState('ae_leads_loc', leads_loc_defaults, { export const leads_loc = new PersistedState('ae_leads_loc', leads_loc_defaults, {
serializer: { serializer: {
serialize: JSON.stringify, // Stamp __version on every write so store_versions.ts's _check_and_wipe() can
deserialize: (raw: string) => ({ ...leads_loc_defaults, ...JSON.parse(raw) }) // detect a breaking schema change and clear stale browsers on next load. Found
// 2026-06-16: this was previously bare JSON.stringify with no __version field,
// which made _check_and_wipe('ae_leads_loc', ...) see undefined !== expected
// every time and wipe this store on EVERY page load. This import also guarantees
// store_versions.ts's wipe side-effect runs before this PersistedState reads
// from localStorage (ES module execution order).
serialize: (value) =>
JSON.stringify({ ...value, __version: AE_LEADS_LOC_VERSION }),
deserialize: (raw: string) => {
const { __version, ...stored } = JSON.parse(raw);
return { ...leads_loc_defaults, ...stored };
}
} }
}); });

View File

@@ -14,10 +14,19 @@
*/ */
import { PersistedState } from 'runed'; import { PersistedState } from 'runed';
import { pres_mgmt_loc_defaults } from './ae_events_stores__pres_mgmt_defaults'; import { pres_mgmt_loc_defaults } from './ae_events_stores__pres_mgmt_defaults';
import { AE_PRES_MGMT_LOC_VERSION } from './store_versions';
export const pres_mgmt_loc = new PersistedState('ae_pres_mgmt_loc', pres_mgmt_loc_defaults, { export const pres_mgmt_loc = new PersistedState('ae_pres_mgmt_loc', pres_mgmt_loc_defaults, {
serializer: { serializer: {
serialize: JSON.stringify, // Stamp __version on every write so store_versions.ts's _check_and_wipe() can
deserialize: (raw: string) => ({ ...pres_mgmt_loc_defaults, ...JSON.parse(raw) }) // detect a breaking schema change and clear stale browsers on next load. This
// import also guarantees store_versions.ts's wipe side-effect runs before this
// PersistedState reads from localStorage (ES module execution order).
serialize: (value) =>
JSON.stringify({ ...value, __version: AE_PRES_MGMT_LOC_VERSION }),
deserialize: (raw: string) => {
const { __version, ...stored } = JSON.parse(raw);
return { ...pres_mgmt_loc_defaults, ...stored };
}
} }
}); });

View File

@@ -86,10 +86,6 @@ export interface PressMgmtRemoteCfg {
// Fields synced from remote are overwritten on every event load when lock_config=true. // Fields synced from remote are overwritten on every event load when lock_config=true.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface PresMgmtLocState { export interface PresMgmtLocState {
// --- System / lock state (mirrored from remote for display) ---
sync_local_config: boolean;
lock_config: boolean;
// --- Query / search preferences --- // --- Query / search preferences ---
use_12h: boolean; use_12h: boolean;
qry_enabled: 'all' | 'not_enabled' | 'enabled'; qry_enabled: 'all' | 'not_enabled' | 'enabled';
@@ -268,9 +264,6 @@ export interface PresMgmtSessState {
// Persisted pres_mgmt local config — survives browser sessions. // Persisted pres_mgmt local config — survives browser sessions.
export const pres_mgmt_loc_defaults: PresMgmtLocState = { export const pres_mgmt_loc_defaults: PresMgmtLocState = {
// System / lock state
sync_local_config: false,
lock_config: false,
// Query / search // Query / search
use_12h: true, use_12h: true,

View File

@@ -120,11 +120,16 @@ if (
_check_and_wipe('ae_loc', AE_LOC_VERSION); _check_and_wipe('ae_loc', AE_LOC_VERSION);
_check_and_wipe('ae_events_loc', AE_EVENTS_LOC_VERSION); _check_and_wipe('ae_events_loc', AE_EVENTS_LOC_VERSION);
_check_and_wipe('ae_idaa_loc', AE_IDAA_LOC_VERSION); _check_and_wipe('ae_idaa_loc', AE_IDAA_LOC_VERSION);
// ae_leads_loc (PersistedState, runed) stamps __version itself in its custom
// serializer — see ae_events_stores__leads.svelte.ts. FIXED 2026-06-16: this was
// previously bare JSON.stringify with no __version field, so this check always saw
// undefined !== expected and wiped ae_leads_loc on EVERY page load.
_check_and_wipe('ae_leads_loc', AE_LEADS_LOC_VERSION); _check_and_wipe('ae_leads_loc', AE_LEADS_LOC_VERSION);
// ae_pres_mgmt_loc uses PersistedState (runed) which stores raw JSON without a __version // ae_pres_mgmt_loc (PersistedState, runed) stamps __version itself in its custom
// field. The _check_and_wipe mechanism requires __version in the stored data — do NOT // serializer — see ae_events_stores__pres_mgmt.svelte.ts. Importing that module runs
// add it here until pres_mgmt_loc_defaults includes __version. For now the key is new // this file's side effects first (ES module order), so this check sees real stored
// (no stale data exists) so no wipe is needed. // data, not a race against the PersistedState constructor.
_check_and_wipe('ae_pres_mgmt_loc', AE_PRES_MGMT_LOC_VERSION);
} }
function _check_and_wipe(key: string, expected_version: number): void { function _check_and_wipe(key: string, expected_version: number): void {

View File

@@ -147,7 +147,23 @@ async function save() {
fields: { mod_pres_mgmt_json: draft }, fields: { mod_pres_mgmt_json: draft },
log_lvl: 1 log_lvl: 1
}); });
// Reload the event so the sync function picks up the new config
// Sync this browser's pres_mgmt_loc immediately from the just-saved draft.
// WHY: load_ae_obj_id__event() below is SWR — it returns the stale Dexie
// cache right away and refreshes from the API in the background (fire and
// forget), so it cannot be relied on to update pres_mgmt_loc in time. Calling
// the sync function directly here makes the editor's own browser update
// deterministically, with no race against Dexie/liveQuery propagation.
events_func.sync_config__event_pres_mgmt({
pres_mgmt_cfg_remote: draft,
log_lvl: 1
});
// Also kick off the normal SWR reload (try_cache: true, the default) so
// Dexie gets the fresh record in the background — this is what lets OTHER
// browsers/tabs pick up the change next time they query Dexie. Do not pass
// try_cache: false here — that skips the Dexie write entirely (see
// GUIDE__SvelteKit2_Svelte5_DexieJS.md "try_cache: false Bug").
await events_func.load_ae_obj_id__event({ await events_func.load_ae_obj_id__event({
api_cfg: $ae_api, api_cfg: $ae_api,
event_id: event_id, event_id: event_id,