CS Pathway Game - MVC Architecture
MVC architecture and file organization for the CS Pathway Game project
Overview
╔════════════════════════════════════════════════════════════════════════════╗
║ PROFILE SYSTEM - MVC ARCHITECTURE ║
╚════════════════════════════════════════════════════════════════════════════╝
## File Organization
The CS Pathway game implements MVC architecture across the following file structure:
┌─────────────────────────────────────────────────────────────────────────────┐
│ CS PATHWAY GAME ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📂 /_notebooks/home/ [ENTRY POINT] │
│ └─ 2026-04-02-home2-gamified-mvp.ipynb │
│ • Launches the CS Pathway Game │
│ • Provides journey from landing page to gamified course intro │
│ • Integrates all GameLevelCSPath*.js files │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ 📂 /_projects/cs-pathway-game/levels/ [CONTROLLER + VIEW] │ │
│ │ │ │
│ │ Game Level Controllers: │ │
│ │ ├─ GameLevelCSPath0Forge.js Level 0: Identity Forge │ │
│ │ │ • Course entry and PII lesson │ │
│ │ │ • Built-in sign-up / login flow (no redirect required) │ │
│ │ │ • Establishes OCS account (Flask + MySQL integration) │ │
│ │ │ • Enables PII viewing and theming customization │ │
│ │ │ │ │
│ │ ├─ GameLevelCSPath1Way.js Level 1: Wayfinding World │ │
│ │ │ • Social classroom connection and registration │ │
│ │ │ • Introduces blogging and "about me" coding foundations │ │
│ │ │ • Self-evaluation checkpoint (passport to next level) │ │
│ │ │ │ │
│ │ ├─ GameLevelCSPath2Mission.js Level 2: Mission Tools │ │
│ │ │ • Personal computer setup and local SDLC workflow │ │
│ │ │ • Transition from online-only to local development │ │
│ │ │ • Tools: GitHub, VSCode, Browser, Terminal, Make │ │
│ │ │ • Prepares student for PBL-based CompSci learning │ │
│ │ │ │ │
│ │ └─ GameLevelCSPathIdentity.js Shared Base Class │ │
│ │ • Shares Identity Forge data across all levels │ │
│ │ • Helper class extended by Wayfinding and Mission │ │
│ │ • Simplifies profile management across game progression │ │
│ │ │ │
│ │ Game Engine Essentials (View Components): │ │
│ │ └─ essentials/* │ │
│ │ • StatusPanel.js - Player profile display │ │
│ │ • FormPanel.js - Identity Terminal input forms │ │
│ │ • Picker.js - Avatar and theme selectors │ │
│ │ • DialogueSystem - NPC interactions │ │
│ │ • Core rendering and UI abstraction │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ 📂 /_projects/cs-pathway-game/model/ [MODEL] │ │
│ │ │ │
│ │ Data Persistence Bridge: │ │
│ │ ├─ ProfileManager.js Profile Orchestrator │ │
│ │ │ • Unified persistence manager (abstraction layer) │ │
│ │ │ • JSON in / JSON out interface for View and Controller │ │
│ │ │ • Coordinates localStorage speed + backend reliability │ │
│ │ │ • Maps data to OCS User table columns efficiently │ │
│ │ │ │ │
│ │ ├─ localProfile.js Primary Storage │ │
│ │ │ • localStorage management (instant, client-side) │ │
│ │ │ • Source of truth for all users │ │
│ │ │ │ │
│ │ └─ persistentProfile.js Secondary Storage │ │
│ │ • Flask API fetch request/response layer │ │
│ │ • Backend analytics and cross-device recovery │ │
│ │ • Instructor dashboard data │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ Design Philosophy: │
│ • Immersive experience minimizing text/walls of instructions │
│ • Exploratory, game-based onboarding to course concepts │
│ • Project-Based Learning (PBL) preparation through gaming │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
### Architecture Summary
**Clean MVC architecture** with **localStorage-first persistence** and **async backend analytics**.
- **Model**: All user state lives in localStorage for instant performance
- **View**: GameEngine essentials provide reusable UI components
- **Controller**: Game level classes orchestrate progression and business logic
- **Backend**: Flask API provides instructor analytics and cross-device recovery
┌─────────────────────────────────────────────────────────────────────────────┐
│ MODEL LAYER (Data & Persistence) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ProfileManager.js │ │
│ │ ────────────────── │ │
│ │ localStorage-first with async backend analytics sync │ │
│ │ │ │
│ │ ✓ async initialize() - Load from localStorage, check auth │ │
│ │ ✓ async saveIdentity() - Write localStorage + async backend │ │
│ │ ✓ async saveAvatar() - Write localStorage + async backend │ │
│ │ ✓ async saveTheme() - Write localStorage + async backend │ │
│ │ ✓ async updateProgress() - Write localStorage + async backend │ │
│ │ ✓ exists() / getProfile() - Read from localStorage only │ │
│ │ ✓ async clear() - Clear localStorage, reset backend │ │
│ │ ✓ getAuthStatus() - Return auth state + sync health │ │
│ │ │ │
│ │ Properties: │ │
│ │ • isAuthenticated: boolean (/api/id status) │ │
│ │ • syncFailureCount: number (analytics sync errors) │ │
│ │ • lastSyncTime: timestamp (last successful backend write) │ │
│ └──────────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ │ ALWAYS │
│ ┌──────────────────────────▼───────────────┐ │
│ │ localProfile.js │ │
│ │ ──────────────── │ │
│ │ PRIMARY: Source of truth for ALL users │ │
│ │ │ │
│ │ Storage: │ │
│ │ • localStorage (instant, reliable) │ │
│ │ • Key: ocs_local_profile │ │
│ │ • Structure: flat top-level fields │ │
│ │ name, email, githubID + game_profile │ │
│ │ • game_profile: blob organized by level │ │
│ │ - identity-forge (includes avatar) │ │
│ │ - wayfinding-world │ │
│ │ - mission-tooling │ │
│ │ • Includes: lastModified timestamp │ │
│ │ │ │
│ │ Methods: │ │
│ │ • save(data) - Sync write │ │
│ │ • load() - Sync read │ │
│ │ • update(partial) - Merge and save │ │
│ │ • clear() - FULL WIPE │ │
│ │ • exists() - Check presence │ │
│ │ • getFlatProfile() - Flatten structure│ │
│ └───────────────────┬──────────────────────┘ │
│ │ │
│ │ IF AUTHENTICATED (async, non-blocking) │
│ │ │
│ ┌───────────────────▼──────────────────────────────────┐ │
│ │ persistentProfile.js │ │
│ │ ───────────────── │ │
│ │ SECONDARY: Analytics copy for instructors │ │
│ │ │ │
│ │ Storage: │ │
│ │ • Python Flask API (SQLAlchemy) │ │
│ │ • Endpoints: /api/profile/game, /api/id │ │
│ │ • Database: users table (existing) │ │
│ │ - id (primary key, not in localStorage) │ │
│ │ - _name, _email (mapped from localStorage) │ │
│ │ - _uid (stores githubID from localStorage) │ │
│ │ - _sid, _password, _role, _pfp, _school │ │
│ │ - _grade_data, _ap_exam, _class (JSON) │ │
│ │ - _game_profile (NEW JSON column for game data) │ │
│ │ • _game_profile structure matches localStorage │ │
│ │ │ │
│ │ Auth: JWT cookies required │ │
│ │ │ │
│ │ Methods (async, best-effort): │ │
│ │ • static async load() - Fetch from backend │ │
│ │ • static async save(data) - Upload with CRC │ │
│ │ • static async update(data) - Partial update │ │
│ │ • static async clear() - Reset (keeps ID) │ │
│ │ └─> PRESERVES id, _name, _email, _uid │ │
│ │ only clears _game_profile (game data) │ │
│ │ • static async isAuthenticated() - Check JWT │ │
│ │ • static async getFlatProfile() - Backend read │ │
│ │ │ │
│ │ Purpose: │ │
│ │ ✓ Instructor dashboard analytics │ │
│ │ ✓ Cross-device recovery │ │
│ │ ✓ Progress tracking and reporting │ │
│ │ ✗ NOT required for game to function │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ VIEW LAYER (GameEngine UI Components) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ StatusPanel (GameEngine Component) │ │
│ │ ─────────────────────────────────── │ │
│ │ Used for: PLAYER PROFILE display │ │
│ │ Location: GameLevelCSPath*.js │ │
│ │ │ │
│ │ Current Configuration: │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ PLAYER PROFILE │ │ │
│ │ │ ────────────── │ │ │
│ │ │ Name: John M │ │ │
│ │ │ Email: jmort1021@gmail.com │ │ │
│ │ │ GitHub: jm1021 │ │ │
│ │ │ │ │ │
│ │ │ Avatar Sprite │ │ │
│ │ │ ────────────── │ │ │
│ │ │ Sprite: Miku │ │ │
│ │ │ │ │ │
│ │ │ World Theme │ │ │
│ │ │ ──────────── │ │ │
│ │ │ Theme: Forest │ │ │
│ │ │ │ │ │
│ │ │ ───────────────────────────────────────────── │ │ │
│ │ │ 🔄 Reset Profile │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Component: this.profilePanelView = new StatusPanel({...}) │ │
│ │ Updates via: this.updateProfilePanel(profileData) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ FormPanel (GameEngine Component) │ │
│ │ ───────────────────────────────── │ │
│ │ Used for: Identity Terminal input │ │
│ │ Location: GameLevelCSPath*.js │ │
│ │ │ │
│ │ Configuration: │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ ⚔ Identity Terminal Setup │ │ │
│ │ │ ────────────────────────── │ │ │
│ │ │ You're logged in. Enter your profile info below. │ │ │
│ │ │ │ │ │
│ │ │ Name: [________________] │ │ │
│ │ │ Email: [________________] │ │ │
│ │ │ GitHub Username: [______] │ │ │
│ │ │ │ │ │
│ │ │ [Unlock Identity Terminal] [Cancel] │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Component: this.identityFormView = new FormPanel({...}) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ AuthFlow Panel (Custom - GameLevelCsPath0Forge.js) │ │
│ │ ────────────────────────────────────────────────── │ │
│ │ Used for: Built-in sign-up and login inside the terminal │ │
│ │ Location: GameLevelCSPath0Forge.js (this.showAuthFlow) │ │
│ │ │ │
│ │ Flow: │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ ⚔ IDENTITY TERMINAL │ │ │
│ │ │ ──────────────────────────────────────────────────── │ │ │
│ │ │ To register your identity, you need an account. │ │ │
│ │ │ │ │ │
│ │ │ [Log In] [Sign Up] [Cancel] │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │ │
│ │ ┌────▼────┐ ┌─────▼──────┐ │ │
│ │ │ LOG IN │ │ CREATE ACCT│ │ │
│ │ │ uid+pw │ │ full form │ │ │
│ │ │ →/api/ │ │ →/api/user │ │ │
│ │ │ authen- │ │ then auto- │ │ │
│ │ │ ticate │ │ fills login│ │ │
│ │ └────┬────┘ └─────┬──────┘ │ │
│ │ └──────────┬──────────┘ │ │
│ │ ▼ │ │
│ │ resolve(true) → Identity Form opens │ │
│ │ │ │
│ │ API calls: │ │
│ │ • POST /api/authenticate (login) │ │
│ │ • POST /api/user (signup → Flask) │ │
│ │ • POST /api/person/create (signup → Spring, non-blocking) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Picker (GameEngine Component) │ │
│ │ ────────────────────────────── │ │
│ │ Used for: Avatar selection & World Theme selection │ │
│ │ Location: GameLevelCSPath*.js │ │
│ │ │ │
│ │ Avatar Picker Configuration: │ │
│ │ • Title: ⚔ Avatar Forge Sprite Selector │ │
│ │ • Grid of sprite previews with live preview on tap │ │
│ │ • Component: this.avatarPickerView = new Picker({...}) │ │
│ │ │ │
│ │ World Theme Picker Configuration: │ │
│ │ • Title: 🌐 World Theme Portal │ │
│ │ • Grid of background previews │ │
│ │ • Component: this.worldThemePickerView = new Picker({...}) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ DialogueSystem (GameEngine Component) │ │
│ │ ────────────────────────────────────── │ │
│ │ Used for: NPC interactions and level guidance │ │
│ │ Location: GameLevelCSPath*.js │ │
│ │ │ │
│ │ Features: │ │
│ │ • Typewriter effect for text display │ │
│ │ • Optional voice synthesis │ │
│ │ • Sequential dialogue queuing │ │
│ │ • Component: this.levelDialogueSystem = new DialogueSystem() │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Toast Messages (Custom Implementation) │ │
│ │ ─────────────────────────────────────── │ │
│ │ Used for: Status updates and notifications │ │
│ │ Location: GameLevelCSPath*.js │ │
│ │ │ │
│ │ Example: │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ ✦ Profile saved successfully │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Usage: this.showToast('✦ Profile saved') │ │
│ │ Auto-dismisses after 3 seconds │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ Status & Error Indicators (FUTURE - After Backend API): │
│ ─────────────────────────────────────────────────────── │
│ Option 1: Extend StatusPanel with additional status fields │
│ • Add fields for: Registration, Online, Last Sync, Activity Log │
│ • Use HTTP status codes: 🟢 200, ⚠️ 401/4xx, 🔴 5xx/ERR │
│ • Update via: profileManager.getStatus() → updateProfilePanel() │
│ │
│ Option 2: Add toast messages for sync events │
│ • Show: "✦ Profile synced (200)" on successful backend sync │
│ • Show: "⚠️ Offline - local only (401)" when not authenticated │
│ • Show: "🔴 Sync failed (500)" on backend errors │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONTROLLER LAYER (Game Logic) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ GameLevelCSPath*.js (Game Level Controllers) │ │
│ │ ────────────────────────────────────────────── │ │
│ │ Orchestrates game flow and user interactions │ │
│ │ │ │
│ │ Responsibilities: │ │
│ │ • Initialize ProfileManager │ │
│ │ • Gate Identity Terminal behind built-in auth flow │ │
│ │ • Handle form submissions │ │
│ │ • Call ProfileManager methods │ │
│ │ • Update game state based on profile │ │
│ │ • Trigger level unlocks │ │
│ │ │ │
│ │ Identity Terminal Flow (Level 0): │ │
│ │ 1. Player approaches Identity Gatekeeper NPC │ │
│ │ 2. runIdentityTerminal() checks PersistentProfile.isAuth() │ │
│ │ 3. Not logged in → showAuthFlow() (sign-up or login in-game) │ │
│ │ 4. Auth success → showIdentityForm() (name, email, github) │ │
│ │ 5. Submit → saveIdentity() + updateIdentityProgress() │ │
│ │ 6. identityUnlocked = true, World Theme Portal gate opens │ │
│ │ │ │
│ │ General Flow: │ │
│ │ 1. constructor() - Create ProfileManager instance │ │
│ │ 2. async init() - Call profileManager.initialize() │ │
│ │ 3. Restore state if profile exists │ │
│ │ 4. Handle user actions → save via ProfileManager │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Profile Data Structure
localStorage Structure (ocs_local_profile)
{
// Top-level identity fields (flattened for easy access)
"name": "John M",
"email": "jmort1021@gmail.com",
"githubID": "jm1021", // Note: githubID not github
// Metadata
"version": "1.0",
"localId": "local_1775746188941_biguq1t",
"createdAt": "2026-04-09T14:49:48.941Z",
"updatedAt": "2026-04-09T14:50:41.786Z",
// Game data organized by level
"game_profile": {
"identity-forge": {
"preferences": {
"sprite": "Miku",
"spriteMeta": {
"src": "/images/platformer/sprites/miku.png",
"width": 46,
"height": 52.5
}
},
"progress": {
"identityUnlocked": true,
"avatarSelected": true
},
"completedAt": "2026-04-09T14:49:52.000Z"
},
"wayfinding-world": {
"preferences": {
"theme": "Forest"
},
"progress": {
"worldThemeSelected": true,
"navigationComplete": false
},
"completedAt": null
},
"mission-tooling": {
"progress": {
"toolsUnlocked": false
},
"completedAt": null
}
}
}
Backend Database Structure (users table - SQLAlchemy)
# Existing users table with new _game_profile column
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
_name = db.Column(db.String(255), unique=False, nullable=False)
_uid = db.Column(db.String(255), unique=True, nullable=False) # stores githubID
_email = db.Column(db.String(255), unique=False, nullable=False)
_sid = db.Column(db.String(255), unique=False, nullable=True) # student ID
_password = db.Column(db.String(255), unique=False, nullable=False)
_role = db.Column(db.String(20), default="User", nullable=False)
_pfp = db.Column(db.String(255), unique=False, nullable=True) # profile pic
_grade_data = db.Column(db.JSON, unique=False, nullable=True)
_ap_exam = db.Column(db.JSON, unique=False, nullable=True)
_class = db.Column(db.JSON, unique=False, nullable=True)
_school = db.Column(db.String(255), default="Unknown", nullable=True)
# NEW: Gamified home interface data
_game_profile = db.Column(db.JSON, unique=False, nullable=True)
_game_profile JSON structure (stored in users table):
{
"version": "1.0",
"localId": "local_1775746188941_biguq1t", // Preserved from localStorage
"createdAt": "2026-04-09T14:49:48.941Z",
"updatedAt": "2026-04-09T14:50:41.786Z",
"lastModified": 1680000000000, // Timestamp for sync conflict resolution
// Game levels (matches localStorage structure)
"identity-forge": {
"preferences": { "sprite": "Miku", "spriteMeta": {...} },
"progress": { "identityUnlocked": true, "avatarSelected": true },
"completedAt": "2026-04-09T14:49:52.000Z"
},
"wayfinding-world": {
"preferences": { "theme": "Forest" },
"progress": { "worldThemeSelected": true, "navigationComplete": false },
"completedAt": null
},
"mission-tooling": {
"progress": { "toolsUnlocked": false },
"completedAt": null
}
}
Data Flow: localStorage ↔ Backend
// Save to localStorage (immediate)
const profile = {
name: "John M",
email: "jmort1021@gmail.com",
githubID: "jm1021",
version: "1.0",
localId: "local_1775746188941_biguq1t",
createdAt: "2026-04-09T14:49:48.941Z",
updatedAt: Date.now(),
game_profile: {
"identity-forge": { /* preferences + progress */ },
"wayfinding-world": { /* preferences + progress */ },
"mission-tooling": { /* progress */ }
}
};
localStorage.setItem('ocs_local_profile', JSON.stringify(profile));
// Sync to backend (async, if authenticated)
if (isAuthenticated) {
// Map to users table structure
await fetch('/api/profile/game', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
_name: profile.name,
_email: profile.email,
_uid: profile.githubID, // githubID maps to _uid column
_game_profile: {
version: profile.version,
localId: profile.localId,
createdAt: profile.createdAt,
updatedAt: profile.updatedAt,
lastModified: Date.now(),
...profile.game_profile // Spread game-level data
}
})
});
}
Key Design Decisions
- Integrated with existing users table:
- Game data is another view of student (user-friendly interface)
- Leverages existing authentication and user management
- _name, _email, _uid already exist for identity
- _game_profile added as new JSON column
- _uid stores githubID:
- localStorage uses githubID, maps to _uid in backend
- Unique constraint on _uid for user lookups
- Consistent with existing user table structure
- _game_profile for game data:
- Flexible schema - add new game levels without DB migrations
- Three levels: identity-forge, wayfinding-world, mission-tooling
- Avatar selection is part of identity-forge (not separate level)
- JSON structure mirrors localStorage for easy sync
- id vs localId:
- Backend uses id as primary key (existing users table)
- localStorage uses localId for tracking (anonymous/guest users)
- localId preserved in _game_profile for analytics
- Other user columns remain untouched:
- _grade_data, _ap_exam, _class continue to function
- Game interface is additive, doesn’t replace existing data
- Instructor can see both traditional gradebook and game progress
Initialization Flow (localStorage-First)
┌─────────────────────────────────────────────────────────────────────────────┐
│ ProfileManager.initialize() - localStorage-First Load │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ START │
│ │ │
│ ├─> STEP 1: ALWAYS try localStorage first (instant) │
│ │ │ │
│ │ ├─> LocalProfile.load() │
│ │ │ └─> localStorage.getItem('ocs_local_profile') │
│ │ │ │
│ │ ├─────┬─> Profile exists in localStorage │
│ │ │ │ │ │
│ │ │ │ ├─> Parse and validate data │
│ │ │ │ ├─> Store: this.profile = data │
│ │ │ │ └─> GO TO STEP 2 (check auth for sync) │
│ │ │ │ │
│ │ │ └─> localStorage empty (new device or cleared) │
│ │ │ └─> GO TO STEP 2 (may restore from backend) │
│ │ │ │
│ │ │ │
│ ├─> STEP 2: Check authentication status (for analytics) │
│ │ │ │
│ │ ├─> await PersistentProfile.isAuthenticated() │
│ │ │ └─> GET /api/id (check JWT cookie) │
│ │ │ │
│ │ ├─────┬─> 200 OK: User authenticated │
│ │ │ │ │ │
│ │ │ │ ├─> Set: isAuthenticated = true │
│ │ │ │ ├─> Update widget: 🟢 Analytics ON │
│ │ │ │ │ │
│ │ │ │ └─> If localStorage was empty: │
│ │ │ │ │ │
│ │ │ │ ├─> Try: await PersistentProfile.load() │
│ │ │ │ │ └─> GET /api/profile/game │
│ │ │ │ │ │
│ │ │ │ ├─────┬─> Backend has profile (recovery scenario) │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ ├─> Restore to localStorage │
│ │ │ │ │ │ │ └─> LocalProfile.save(backendData) │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ ├─> Store: this.profile = backendData │
│ │ │ │ │ │ └─> Return: backendData (device recovery!) │
│ │ │ │ │ │ │
│ │ │ │ │ └─> Backend empty (new authenticated user) │
│ │ │ │ │ └─> Return: null (start fresh) │
│ │ │ │ │ │
│ │ │ │ └─> localStorage already had data: │
│ │ │ │ │ │
│ │ │ │ ├─> Compare timestamps │
│ │ │ │ ├─> Use NEWER data (localStorage usually wins) │
│ │ │ │ └─> Sync newer data to backend (best-effort) │
│ │ │ │ │
│ │ │ └─> 401 Unauthorized: Guest user │
│ │ │ │ │
│ │ │ ├─> Set: isAuthenticated = false │
│ │ │ ├─> Update widget: ⚠️ 401 (Unregistered) │
│ │ │ └─> Continue with localStorage data (if any) │
│ │ │ │
│ │ │ │
│ └─> RETURN: this.profile (from localStorage or recovered from backend) │
│ │
│ KEY PRINCIPLE: localStorage is always authoritative. │
│ Backend is for analytics & recovery only. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
New Device Recovery Scenario
Scenario: Student logs in from school computer after working at home.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Device Recovery Flow: Backend → localStorage │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ DEVICE A (Home Computer): │
│ ┌────────────────────────────────────────────┐ │
│ │ localStorage: ocs_local_profile │ │
│ │ { │ │
│ │ name: "Alice", │ │
│ │ email: "alice@example.com", │ │
│ │ githubID: "alice2024", │ │
│ │ localId: "local_1234567890_abc", │ │
│ │ createdAt: "2026-04-09T10:00:00Z", │ │
│ │ updatedAt: "2026-04-09T14:30:00Z", │ │
│ │ game_profile: { │ │
│ │ "identity-forge": { │ │
│ │ preferences: { sprite: "Knight", spriteMeta: {...} }, │
│ │ progress: { │ │
│ │ identityUnlocked: true, │ │
│ │ avatarSelected: true │ │
│ │ } │ │
│ │ }, │ │
│ │ "wayfinding-world": { │ │
│ │ preferences: { theme: "Forest" }, │ │
│ │ progress: { worldThemeSelected: true }│ │
│ │ }, │ │
│ │ "mission-tooling": { │ │
│ │ progress: { toolsUnlocked: false } │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ │ (User was logged in, async sync succeeded) │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Backend Database: users table │ │
│ │ { │ │
│ │ id: 42, │ │
│ │ _name: "Alice", │ │
│ │ _email: "alice@example.com", │ │
│ │ _uid: "alice2024", // githubID │ │
│ │ _sid: "12345", │ │
│ │ _role: "Student", │ │
│ │ _school: "Del Norte High School", │ │
│ │ _game_profile: { // JSON column │ │
│ │ localId: "local_1234567890_abc", │ │
│ │ createdAt: "2026-04-09T10:00:00Z", │ │
│ │ updatedAt: "2026-04-09T14:30:00Z", │ │
│ │ lastModified: 1680000000000, │ │
│ │ "identity-forge": { │ │
│ │ preferences: { sprite: "Knight", ... }, │
│ │ progress: { │ │
│ │ identityUnlocked: true, │ │
│ │ avatarSelected: true │ │
│ │ } │ │
│ │ }, │ │
│ │ "wayfinding-world": { │ │
│ │ progress: { worldThemeSelected: true }│ │
│ │ }, │ │
│ │ "mission-tooling": { │ │
│ │ progress: { toolsUnlocked: false } │ │
│ │ } │ │
│ │ }, │ │
│ │ _grade_data: {...}, // existing columns │ │
│ │ _ap_exam: {...}, │ │
│ │ _class: {...} │ │
│ │ } │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ │ │
│ ═══════════════════════════════════════════════════════════════ │
│ │ │
│ DEVICE B (School Computer): │
│ Student logs in → ProfileManager.initialize() runs │
│ │ │
│ ├─> STEP 1: Check localStorage │
│ │ └─> EMPTY (new device, never used) │
│ │ │
│ ├─> STEP 2: Check authentication │
│ │ ├─> GET /api/id → 200 OK (user logged in) │
│ │ └─> isAuthenticated = true │
│ │ │
│ ├─> STEP 3: Recovery - Load from backend │
│ │ │ │
│ │ ├─> GET /api/profile/game │
│ │ │ └─> Returns profile from Device A │
│ │ │ │
│ │ ├─> Restore to Device B localStorage │
│ │ │ └─> LocalProfile.save(backendData) │
│ │ │ │
│ │ └─> User sees their progress! (seamless) │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Device B localStorage: ocs_local_profile │ │
│ │ { │ │
│ │ name: "Alice", ← From Device A (backend) │
│ │ email: "alice@example.com", ← From Device A (backend) │
│ │ githubID: "alice2024", ← From Device A (backend) │
│ │ localId: "local_1234567890_abc", ← Preserved from original! │
│ │ createdAt: "2026-04-09T10:00:00Z", ← Original timestamp │
│ │ updatedAt: "2026-04-09T14:30:00Z", ← Last update timestamp │
│ │ game_profile: { ← From Device A │
│ │ "identity-forge": { │ │
│ │ preferences: { sprite: "Knight", ... }, │
│ │ progress: { │ │
│ │ identityUnlocked: true, │ │
│ │ avatarSelected: true │ │
│ │ } │ │
│ │ }, │ │
│ │ "wayfinding-world": { │ │
│ │ progress: { worldThemeSelected: true }│ │
│ │ }, │ │
│ │ "mission-tooling": { │ │
│ │ progress: { toolsUnlocked: false } │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ ✓ All progress restored instantly │
│ ✓ Original localId preserved for analytics │
│ ✓ Student continues from where they left off │
│ ✓ Device B now syncs changes back to backend │
│ │
│ CRITICAL USE CASE (Home ↔ School): │
│ • Student works at home → logs in → progress synced to backend │
│ • Next day at school → logs in different computer → progress restored │
│ • Completes assignments at school → synced to backend │
│ • Takes work home again → already has latest from school session │
│ • Seamless cross-device experience without manual exports/imports │
│ • Instructor sees consolidated activity across all devices │
│ │
│ SUBSEQUENT SAVES on Device B: │
│ │ │
│ ├─> localStorage.save() (instant) │
│ └─> Backend.save() (async analytics) │
│ └─> Instructor sees latest from either device │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Key Points:
- Backend as Recovery Layer: The backend stores the last known state for device recovery
- localStorage Always Wins: Once recovered, Device B’s localStorage becomes authoritative
- Transparent to Student: They just log in and continue - no manual import needed
- Instructor Benefits: Backend aggregates activity across all devices
- Cookie Loss Recovery: If user clears cookies but localStorage intact, they can re-authenticate and sync resumes
Backend Analytics Sync (Continuous)
┌─────────────────────────────────────────────────────────────────────────────┐
│ Ongoing Analytics Sync - How Instructor Gets Data │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ STUDENT ACTIVITY (Any Device): │
│ │
│ User Action → localStorage (instant) ────┐ │
│ │ │
│ ├─> GUI updates │
│ │ Game continues immediately │
│ │ │
│ IF authenticated: ───────────────────────┘ │
│ │ │
│ ├─> async PUT /api/profile/game │
│ │ ├─> Payload: { data, lastModified: timestamp } │
│ │ │ │
│ │ ├─> Backend validates │
│ │ │ ├─> Check JWT (user_id) │
│ │ │ ├─> Verify data structure │
│ │ │ └─> Compare timestamps (only save if newer) │
│ │ │ │
│ │ ├─> Save to database: │
│ │ │ UPDATE game_profiles SET │
│ │ │ preferences = ?, │
│ │ │ progress = ?, │
│ │ │ last_modified = ? │
│ │ │ WHERE user_id = ? │
│ │ │ │
│ │ └─> Response: { success: true, saved: data } │
│ │ └─> Frontend verifies (CRC-style check) │
│ │ │
│ └─> On failure: │
│ ├─> Increment syncFailureCount │
│ ├─> Log for debugging │
│ └─> NO user impact (localStorage still has data) │
│ │
│ │
│ INSTRUCTOR DASHBOARD: │
│ │
│ GET /api/instructor/class/progress │
│ │ │
│ ├─> SELECT * FROM game_profiles │
│ │ WHERE user_id IN (class_roster) │
│ │ │
│ └─> Returns aggregated data: │
│ ┌─────────────────────────────────────────┐ │
│ │ Student | Progress | Last Active│ │
│ ├─────────────────────────────────────────┤ │
│ │ Alice | 75% complete | 5 min ago │ │
│ │ Bob | 50% complete | 1 hour ago │ │
│ │ Charlie | 90% complete | Just now │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ✓ Backend has near-real-time student activity │
│ ✓ Instructor sees who's stuck, who's progressing │
│ ✓ Analytics continue even if occasional sync fails │
│ ✓ Student experience never affected by backend issues │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Migration Flow (First Login)
When a local-only user logs in for the first time:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Migration Flow: Local-Only → Authenticated │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ BEFORE LOGIN (Guest Student): │
│ ┌────────────────────────────────────────────┐ │
│ │ localStorage: ocs_local_profile │ │
│ │ { │ │
│ │ localId: "local_1234567890_abc", │ │
│ │ identity: { │ │
│ │ name: "Player1", │ │
│ │ email: "player1@example.com", │ │
│ │ github: "player1" │ │
│ │ }, │ │
│ │ preferences: { │ │
│ │ sprite: "Knight", │ │
│ │ theme: "Forest" │ │
│ │ }, │ │
│ │ progress: { │ │
│ │ identityUnlocked: true, │ │
│ │ avatarForgeDone: true, │ │
│ │ worldThemeDone: true │ │
│ │ }, │ │
│ │ lastModified: 1680000000000 │ │
│ │ } │ │
│ └────────────────────────────────────────────┘ │
│ │
│ STUDENT LOGS IN → ProfileManager.initialize() runs │
│ │ │
│ ├─> STEP 1: Load from localStorage (instant) │
│ │ └─> Profile found with all progress │
│ │ │
│ ├─> STEP 2: Check authentication │
│ │ ├─> GET /api/id → 200 OK (now authenticated) │
│ │ ├─> isAuthenticated = true │
│ │ └─> Widget shows: 🟢 Analytics ON │
│ │ │
│ ├─> STEP 3: Check backend for existing profile │
│ │ ├─> GET /api/profile/game → 404 (new authenticated user) │
│ │ └─> Backend has no profile yet │
│ │ │
│ └─> STEP 4: Sync localStorage to backend (analytics starts) │
│ │ │
│ ├─> Read current localStorage data │
│ ├─> Add authenticated user info from /api/id │
│ ├─> POST to /api/profile/game with merged data │
│ │ └─> Includes: lastModified timestamp │
│ │ │
│ └─> Backend save succeeds │
│ └─> localStorage REMAINS UNTOUCHED (still source of truth) │
│ │
│ AFTER LOGIN: │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ localStorage: UNCHANGED (still primary) │ │
│ │ { │ │
│ │ localId: "local_1234567890_abc", │ │
│ │ identity: { name: "Player1", ... }, │ │
│ │ preferences: { sprite: "Knight", ... }, │ │
│ │ progress: { ... }, │ │
│ │ lastModified: 1680000000000 │ │
│ │ } │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ │ (Async sync established) │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Backend Database: game_profiles (NEW) │ │
│ │ { │ │
│ │ user_id: 123, ← From /api/id JWT │
│ │ identity: { │ │
│ │ name: "John Doe", ← From /api/id │
│ │ email: "john@example.com", ← From /api/id │
│ │ github: "player1" ← From localStorage │
│ │ }, │ │
│ │ preferences: { ← From localStorage │
│ │ sprite: "Knight", │ │
│ │ theme: "Forest" │ │
│ │ }, │ │
│ │ progress: { ← From localStorage │
│ │ identityUnlocked: true, │ │
│ │ avatarForgeDone: true, │ │
│ │ worldThemeDone: true │ │
│ │ }, │ │
│ │ metadata: { │ │
│ │ localId: "local_1234567890_abc", ← PRESERVED for tracking! │
│ │ firstSync: 1680000100000 ← When they logged in │
│ │ }, │ │
│ │ lastModified: 1680000000000 ← Matches localStorage │
│ │ } │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ✓ All progress preserved in localStorage (untouched) │
│ ✓ Backend now has analytics copy │
│ ✓ Original local ID maintained for tracking guest → auth transition │
│ ✓ Seamless - user sees 🟢 indicator, game continues normally │
│ ✓ Future writes go to BOTH: localStorage (instant) + backend (async) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘