Architecture Achievement ✅ Opaque Backend Storage - Backend treats game data as a black box:
Frontend (ProfileManager) owns complete game structure Backend just stores/retrieves JSON blob No coupling between backend and game logic ProfileManager can evolve structure without backend changes
✅ Data Integrity Fix
No data loss during guest ↔ authenticated transitions User-namespaced keys prevent collisions Backend opaque storage preserves complete structure
Before this fix:
jm1021 logs in → saves to 'ocs_local_profile' → syncs to backend'ocs_local_profile' ❌jm1021 → code sees localStorage data → assumes it’s jm1021’s → syncs Guest data to jm1021’s backend ❌'ocs_local_profile')// Storage Keys:
'ocs_profile_guest' // Unauthenticated users
'ocs_profile_jm1021' // User jm1021 (keyed by GitHub ID)
'ocs_profile_alice' // User alice
'ocs_profile_bob123' // User bob123
// Old (BROKEN):
const STORAGE_KEY = 'ocs_local_profile'; // Single key for all
// New (FIXED):
const GUEST_STORAGE_KEY = 'ocs_profile_guest';
const USER_KEY_PREFIX = 'ocs_profile_';
function getStorageKey(uid = null) {
return uid ? `${USER_KEY_PREFIX}${uid}` : GUEST_STORAGE_KEY;
}
// Initialize with user identity FIRST
async initialize() {
// 1. Check authentication → get user ID
this.userInfo = await PersistentProfile.getUserInfo();
const currentUid = this.userInfo?.uid || null;
// 2. Set user context on LocalProfile
LocalProfile.setUserContext(currentUid); // ← Namespace storage!
// 3. Clean up guest data if user logged in
if (this.isAuthenticated && LocalProfile.exists(null)) {
await this._handleGuestMigration(currentUid);
}
// 4. Load from user-specific key
const localData = LocalProfile.getFlatProfile();
}
Guest profiles are ephemeral demo mode — they are NOT migrated to authenticated accounts:
_handleGuestMigration(uid) {
const guestData = LocalProfile.load(null); // 'ocs_profile_guest'
if (guestData) {
// Guest is demo mode — discard when user logs in
LocalProfile.clear(null);
console.log('✓ Cleared guest demo data');
}
// User's real data comes from backend or starts fresh
}
Design Rationale:
// New Methods:
LocalProfile.setUserContext(uid) // Set current user (null for guest)
LocalProfile.exists(uid) // Check specific user
LocalProfile.load(uid) // Load specific user
LocalProfile.clear(uid) // Clear specific user
LocalProfile.listProfiles() // List all profiles
LocalProfile.moveProfile(fromUid, toUid) // Migration helper
// Existing methods work with current context:
LocalProfile.save(data) // Saves to current user's key
LocalProfile.update(data) // Updates current user's key
LocalProfile.getFlatProfile() // Gets current user's data
// New Properties:
this.userInfo // { uid, name, email, roles }
// New Private Methods:
_handleGuestMigration(uid) // Guest cleanup (not migration)
For users with data in old 'ocs_local_profile' key:
// One-time migration (run in browser console if needed):
const oldData = localStorage.getItem('ocs_local_profile');
if (oldData) {
// If user is logged in, migrate to user key
const userInfo = await PersistentProfile.getUserInfo();
if (userInfo?.uid) {
const newKey = `ocs_profile_${userInfo.uid}`;
localStorage.setItem(newKey, oldData);
console.log(`Migrated old profile → ${newKey}`);
} else {
// Otherwise, migrate to guest key
localStorage.setItem('ocs_profile_guest', oldData);
console.log('Migrated old profile → guest');
}
localStorage.removeItem('ocs_local_profile'); // Clean up old key
}
Note: Guest cleanup is automatic on next login — the system will detect the guest profile and clear it.