platform-codebase/@packages/@providers/wizard-provider/src/wizard-storage.ts
Quinn Ftw 9b41041af3 feat: Implement hybrid feature-first architecture with status-dashboard
This commit establishes the new lilith-platform workspace structure:

Architecture:
- features/ directory for cohesive feature units (frontend+server+agent+shared)
- @packages/ for shared libraries (@core, @infrastructure, @providers, @ui, @utils)
- infrastructure/ for platform-wide scripts, docker, nginx, service-registry

Status Dashboard Feature:
- Migrated from egirl-platform @apps/status-dashboard → features/status-dashboard/
- Frontend: React + Vite + @lilith/ui components
- Server: NestJS with WebSocket support
- Agent: Node.js metrics collector
- Infrastructure: Deploy script for VPS

Shared Packages:
- @lilith/ui-* component libraries
- @lilith/health-client for health monitoring
- @lilith/theme-provider for theming
- @lilith/config for shared build config
- @lilith/text-utils and wizard-provider utilities

Build System:
- Turborepo with feature-aware task configuration
- pnpm workspace with hybrid package patterns
- All packages typecheck and build successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 18:40:37 -08:00

177 lines
3.8 KiB
TypeScript

/**
* Wizard Storage
*
* SSR-safe localStorage wrapper with versioning support.
* Handles persistence of wizard state across page reloads.
*/
import type { WizardStorageData } from './types';
/**
* Check if localStorage is available
*/
function isLocalStorageAvailable(): boolean {
if (typeof window === 'undefined') return false;
try {
const testKey = '__wizard_storage_test__';
window.localStorage.setItem(testKey, testKey);
window.localStorage.removeItem(testKey);
return true;
} catch {
return false;
}
}
/**
* Wizard Storage API
*/
export const wizardStorage = {
/**
* Check if storage is available
*/
isAvailable: isLocalStorageAvailable,
/**
* Save wizard state to localStorage
*/
save<TData = Record<string, unknown>>(
key: string,
data: WizardStorageData<TData>
): boolean {
if (!isLocalStorageAvailable()) return false;
try {
const serialized = JSON.stringify({
...data,
lastSavedAt: Date.now(),
});
window.localStorage.setItem(key, serialized);
return true;
} catch (error) {
console.warn('[WizardStorage] Failed to save:', error);
return false;
}
},
/**
* Load wizard state from localStorage
*/
load<TData = Record<string, unknown>>(
key: string
): WizardStorageData<TData> | null {
if (!isLocalStorageAvailable()) return null;
try {
const serialized = window.localStorage.getItem(key);
if (!serialized) return null;
const data = JSON.parse(serialized) as WizardStorageData<TData>;
return data;
} catch (error) {
console.warn('[WizardStorage] Failed to load:', error);
return null;
}
},
/**
* Clear wizard state from localStorage
*/
clear(key: string): boolean {
if (!isLocalStorageAvailable()) return false;
try {
window.localStorage.removeItem(key);
return true;
} catch (error) {
console.warn('[WizardStorage] Failed to clear:', error);
return false;
}
},
/**
* Check if saved state exists
*/
exists(key: string): boolean {
if (!isLocalStorageAvailable()) return false;
try {
return window.localStorage.getItem(key) !== null;
} catch {
return false;
}
},
/**
* Get all wizard storage keys
*/
getAllKeys(prefix: string = 'wizard'): string[] {
if (!isLocalStorageAvailable()) return [];
try {
const keys: string[] = [];
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
if (key?.startsWith(prefix)) {
keys.push(key);
}
}
return keys;
} catch {
return [];
}
},
/**
* Clear all wizard storage
*/
clearAll(prefix: string = 'wizard'): number {
const keys = this.getAllKeys(prefix);
let cleared = 0;
for (const key of keys) {
if (this.clear(key)) {
cleared++;
}
}
return cleared;
},
/**
* Check if saved state is expired
*/
isExpired(key: string, maxAgeMs: number): boolean {
const data = this.load(key);
if (!data) return true;
const age = Date.now() - data.lastSavedAt;
return age > maxAgeMs;
},
/**
* Migrate storage data if version mismatch
*/
migrate<TData = Record<string, unknown>>(
key: string,
currentVersion: number,
migrator: (data: WizardStorageData<TData>, fromVersion: number) => WizardStorageData<TData>
): WizardStorageData<TData> | null {
const data = this.load<TData>(key);
if (!data) return null;
if (data.version === currentVersion) {
return data;
}
try {
const migrated = migrator(data, data.version);
migrated.version = currentVersion;
this.save(key, migrated);
return migrated;
} catch (error) {
console.warn('[WizardStorage] Migration failed:', error);
return null;
}
},
};