chore(boot): sweep orphan migration flags from localStorage

Two one-shot bootstraps left a per-user flag in localStorage so they
wouldn't run twice — and after F7 deleted the helpers themselves
(2a8e8ff98), the flags pointed at code that no longer existed:

  mana.profile.silentTwinRepair.<userId>
  mana.profile.avatarMigration.<userId>

New \`cleanupOrphanMigrationFlags()\` runs once per page load from the
(app) layout's onMount, right after \`restoreClientIdFromDexie()\`.
Cheap (single localStorage scan), idempotent (no-op once swept),
silent on private-mode / quota errors. The known-orphan prefix list
lives in the helper file with deletion-commit refs so it's clear
when each entry can be retired.

Future migration deletions: append the prefix to ORPHAN_KEY_PREFIXES
in the same commit that drops the helper, and the next page load
on every device cleans up.

Closes Punkt 8 of the F1-F7 follow-up audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-27 01:01:35 +02:00
parent cabfd1004d
commit 119cd2cf83
2 changed files with 70 additions and 0 deletions

View file

@ -0,0 +1,66 @@
/**
* One-shot cleanup of orphan localStorage keys left behind by deleted
* migration helpers.
*
* When a one-shot bootstrap (e.g. `repairSilentTwinAvatarRows`) finishes
* the first time, it sets a per-user `mana.profile.<name>.<uid>` flag in
* localStorage so it doesn't run again. After the helper itself is
* deleted (F7 of docs/plans/sync-field-meta-overhaul.md), the flag stays
* behind forever a small chunk of dead state that piles up if more
* helpers come and go.
*
* This module sweeps the known orphan prefixes and removes any matching
* key on every page load. Cheap (a single localStorage scan, capped by
* the browser's quota) and idempotent calling it twice is a no-op
* the second time because the keys are already gone.
*
* Add a new prefix here whenever a migration helper is deleted in a
* future commit, then remove the prefix again once enough time has
* passed that no live device still carries the orphan flag.
*/
/**
* Known dead localStorage prefixes. Keep in chronological order with a
* comment naming the deletion commit so it's clear when the entry can
* be removed once enough time has passed.
*/
const ORPHAN_KEY_PREFIXES: readonly string[] = [
// F7: `repair-silent-twin.ts` deleted in 2a8e8ff98 (sync field-meta
// overhaul). The helper guarded itself with this flag so it'd only
// run once per user; the flag now points at code that no longer
// exists.
'mana.profile.silentTwinRepair.',
// F7: `legacy-avatar.ts` deleted in the same commit, same flag
// pattern, same orphan situation.
'mana.profile.avatarMigration.',
];
/**
* Remove every localStorage key matching one of {@link ORPHAN_KEY_PREFIXES}.
*
* Best-effort: failures (private mode, quota errors, locked storage on
* some browsers) are swallowed silently orphan flags are cosmetic, not
* load-bearing.
*/
export function cleanupOrphanMigrationFlags(): void {
if (typeof localStorage === 'undefined') return;
const toRemove: string[] = [];
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key) continue;
if (ORPHAN_KEY_PREFIXES.some((prefix) => key.startsWith(prefix))) {
toRemove.push(key);
}
}
} catch {
return;
}
for (const key of toRemove) {
try {
localStorage.removeItem(key);
} catch {
// Storage locked / private mode — next reload retries.
}
}
}

View file

@ -70,6 +70,7 @@
stopMemoroLlmWatcher,
} from '$lib/modules/memoro/llm-watcher.svelte';
import { createUnifiedSync, restoreClientIdFromDexie } from '$lib/data/sync';
import { cleanupOrphanMigrationFlags } from '$lib/data/migrations-cleanup';
import { syncBilling } from '$lib/stores/sync-billing.svelte';
import { networkStore } from '$lib/stores/network.svelte';
import { db } from '$lib/data/database';
@ -632,6 +633,9 @@
// so the next push/pull keeps the same identity the server
// already knows.
await restoreClientIdFromDexie();
// Sweep stale localStorage flags from migration helpers that
// have since been deleted (F7 + future cleanups).
cleanupOrphanMigrationFlags();
const getToken = () => authStore.getValidToken();
unifiedSync = createUnifiedSync(SYNC_SERVER_URL, getToken, syncBilling.active);
// Expose on window for SYNC_DEBUG.md (Schritt C). Not a security