Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
404 lines
11 KiB
JavaScript
404 lines
11 KiB
JavaScript
/**
|
|
* Design System Server (DSS) - IndexedDB Storage
|
|
*
|
|
* Browser-side storage for offline-first operation:
|
|
* - Projects, components, tokens (synced from backend)
|
|
* - UI state persistence
|
|
* - Request queue for offline operations
|
|
*
|
|
* Uses IndexedDB for structured data with good performance.
|
|
*/
|
|
|
|
const DB_NAME = 'dss';
|
|
const DB_VERSION = 2;
|
|
|
|
class DssDB {
|
|
constructor() {
|
|
this.db = null;
|
|
this.ready = this.init();
|
|
}
|
|
|
|
// === Initialization ===
|
|
|
|
async init() {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
|
|
request.onerror = () => reject(request.error);
|
|
request.onsuccess = () => {
|
|
this.db = request.result;
|
|
console.log('[DssDB] Database ready');
|
|
resolve(this.db);
|
|
};
|
|
|
|
request.onupgradeneeded = (event) => {
|
|
const db = event.target.result;
|
|
this.createStores(db);
|
|
};
|
|
});
|
|
}
|
|
|
|
createStores(db) {
|
|
// Projects
|
|
if (!db.objectStoreNames.contains('projects')) {
|
|
const store = db.createObjectStore('projects', { keyPath: 'id' });
|
|
store.createIndex('status', 'status', { unique: false });
|
|
store.createIndex('updated_at', 'updated_at', { unique: false });
|
|
}
|
|
|
|
// Components
|
|
if (!db.objectStoreNames.contains('components')) {
|
|
const store = db.createObjectStore('components', { keyPath: 'id' });
|
|
store.createIndex('project_id', 'project_id', { unique: false });
|
|
store.createIndex('name', 'name', { unique: false });
|
|
}
|
|
|
|
// Tokens
|
|
if (!db.objectStoreNames.contains('tokens')) {
|
|
const store = db.createObjectStore('tokens', { keyPath: 'id' });
|
|
store.createIndex('project_id', 'project_id', { unique: false });
|
|
store.createIndex('category', 'category', { unique: false });
|
|
}
|
|
|
|
// Styles
|
|
if (!db.objectStoreNames.contains('styles')) {
|
|
const store = db.createObjectStore('styles', { keyPath: 'id' });
|
|
store.createIndex('project_id', 'project_id', { unique: false });
|
|
store.createIndex('type', 'type', { unique: false });
|
|
}
|
|
|
|
// Sync Queue (offline operations)
|
|
if (!db.objectStoreNames.contains('sync_queue')) {
|
|
const store = db.createObjectStore('sync_queue', { keyPath: 'id', autoIncrement: true });
|
|
store.createIndex('status', 'status', { unique: false });
|
|
store.createIndex('created_at', 'created_at', { unique: false });
|
|
}
|
|
|
|
// Activity Log
|
|
if (!db.objectStoreNames.contains('activity')) {
|
|
const store = db.createObjectStore('activity', { keyPath: 'id', autoIncrement: true });
|
|
store.createIndex('project_id', 'project_id', { unique: false });
|
|
store.createIndex('created_at', 'created_at', { unique: false });
|
|
}
|
|
|
|
// Cache (with TTL)
|
|
if (!db.objectStoreNames.contains('cache')) {
|
|
const store = db.createObjectStore('cache', { keyPath: 'key' });
|
|
store.createIndex('expires_at', 'expires_at', { unique: false });
|
|
}
|
|
|
|
// UI State
|
|
if (!db.objectStoreNames.contains('ui_state')) {
|
|
db.createObjectStore('ui_state', { keyPath: 'key' });
|
|
}
|
|
|
|
// Notifications
|
|
if (!db.objectStoreNames.contains('notifications')) {
|
|
const store = db.createObjectStore('notifications', { keyPath: 'id' });
|
|
store.createIndex('read', 'read', { unique: false });
|
|
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
store.createIndex('type', 'type', { unique: false });
|
|
}
|
|
|
|
console.log('[DssDB] Stores created');
|
|
}
|
|
|
|
// === Generic Operations ===
|
|
|
|
async transaction(storeName, mode = 'readonly') {
|
|
await this.ready;
|
|
return this.db.transaction(storeName, mode).objectStore(storeName);
|
|
}
|
|
|
|
async put(storeName, data) {
|
|
await this.ready;
|
|
return new Promise((resolve, reject) => {
|
|
const tx = this.db.transaction(storeName, 'readwrite');
|
|
const store = tx.objectStore(storeName);
|
|
const request = store.put(data);
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
async get(storeName, key) {
|
|
await this.ready;
|
|
return new Promise((resolve, reject) => {
|
|
const tx = this.db.transaction(storeName, 'readonly');
|
|
const store = tx.objectStore(storeName);
|
|
const request = store.get(key);
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
async getAll(storeName) {
|
|
await this.ready;
|
|
return new Promise((resolve, reject) => {
|
|
const tx = this.db.transaction(storeName, 'readonly');
|
|
const store = tx.objectStore(storeName);
|
|
const request = store.getAll();
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
async getAllByIndex(storeName, indexName, value) {
|
|
await this.ready;
|
|
return new Promise((resolve, reject) => {
|
|
const tx = this.db.transaction(storeName, 'readonly');
|
|
const store = tx.objectStore(storeName);
|
|
const index = store.index(indexName);
|
|
const request = index.getAll(value);
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
async delete(storeName, key) {
|
|
await this.ready;
|
|
return new Promise((resolve, reject) => {
|
|
const tx = this.db.transaction(storeName, 'readwrite');
|
|
const store = tx.objectStore(storeName);
|
|
const request = store.delete(key);
|
|
request.onsuccess = () => resolve(true);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
async clear(storeName) {
|
|
await this.ready;
|
|
return new Promise((resolve, reject) => {
|
|
const tx = this.db.transaction(storeName, 'readwrite');
|
|
const store = tx.objectStore(storeName);
|
|
const request = store.clear();
|
|
request.onsuccess = () => resolve(true);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
async count(storeName) {
|
|
await this.ready;
|
|
return new Promise((resolve, reject) => {
|
|
const tx = this.db.transaction(storeName, 'readonly');
|
|
const store = tx.objectStore(storeName);
|
|
const request = store.count();
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
// === Cache with TTL ===
|
|
|
|
async cacheSet(key, value, ttlSeconds = 300) {
|
|
const data = {
|
|
key,
|
|
value,
|
|
created_at: Date.now(),
|
|
expires_at: Date.now() + (ttlSeconds * 1000)
|
|
};
|
|
return this.put('cache', data);
|
|
}
|
|
|
|
async cacheGet(key) {
|
|
const data = await this.get('cache', key);
|
|
if (!data) return null;
|
|
|
|
// Check expiration
|
|
if (Date.now() > data.expires_at) {
|
|
await this.delete('cache', key);
|
|
return null;
|
|
}
|
|
|
|
return data.value;
|
|
}
|
|
|
|
async cacheClear() {
|
|
return this.clear('cache');
|
|
}
|
|
|
|
async cacheCleanExpired() {
|
|
await this.ready;
|
|
const now = Date.now();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const tx = this.db.transaction('cache', 'readwrite');
|
|
const store = tx.objectStore('cache');
|
|
const index = store.index('expires_at');
|
|
const range = IDBKeyRange.upperBound(now);
|
|
|
|
let count = 0;
|
|
const request = index.openCursor(range);
|
|
|
|
request.onsuccess = (event) => {
|
|
const cursor = event.target.result;
|
|
if (cursor) {
|
|
store.delete(cursor.primaryKey);
|
|
count++;
|
|
cursor.continue();
|
|
} else {
|
|
resolve(count);
|
|
}
|
|
};
|
|
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
// === Projects ===
|
|
|
|
async saveProject(project) {
|
|
return this.put('projects', {
|
|
...project,
|
|
updated_at: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
async getProject(id) {
|
|
return this.get('projects', id);
|
|
}
|
|
|
|
async getProjects() {
|
|
return this.getAll('projects');
|
|
}
|
|
|
|
// === Components ===
|
|
|
|
async saveComponents(projectId, components) {
|
|
for (const comp of components) {
|
|
await this.put('components', {
|
|
id: comp.id || `${projectId}-${comp.name}`,
|
|
project_id: projectId,
|
|
...comp
|
|
});
|
|
}
|
|
return components.length;
|
|
}
|
|
|
|
async getComponents(projectId) {
|
|
return this.getAllByIndex('components', 'project_id', projectId);
|
|
}
|
|
|
|
// === Tokens ===
|
|
|
|
async saveTokens(projectId, tokens) {
|
|
for (const token of tokens) {
|
|
await this.put('tokens', {
|
|
id: `${projectId}-${token.name}`,
|
|
project_id: projectId,
|
|
...token
|
|
});
|
|
}
|
|
return tokens.length;
|
|
}
|
|
|
|
async getTokens(projectId) {
|
|
return this.getAllByIndex('tokens', 'project_id', projectId);
|
|
}
|
|
|
|
async getTokensByCategory(projectId, category) {
|
|
const tokens = await this.getTokens(projectId);
|
|
return tokens.filter(t => t.category === category);
|
|
}
|
|
|
|
// === Sync Queue (Offline Operations) ===
|
|
|
|
async queueOperation(operation) {
|
|
return this.put('sync_queue', {
|
|
...operation,
|
|
status: 'pending',
|
|
created_at: new Date().toISOString(),
|
|
retries: 0
|
|
});
|
|
}
|
|
|
|
async getPendingOperations() {
|
|
return this.getAllByIndex('sync_queue', 'status', 'pending');
|
|
}
|
|
|
|
async markOperationComplete(id) {
|
|
const op = await this.get('sync_queue', id);
|
|
if (op) {
|
|
op.status = 'complete';
|
|
op.completed_at = new Date().toISOString();
|
|
await this.put('sync_queue', op);
|
|
}
|
|
}
|
|
|
|
async markOperationFailed(id, error) {
|
|
const op = await this.get('sync_queue', id);
|
|
if (op) {
|
|
op.status = 'failed';
|
|
op.error = error;
|
|
op.retries = (op.retries || 0) + 1;
|
|
await this.put('sync_queue', op);
|
|
}
|
|
}
|
|
|
|
// === Activity Log ===
|
|
|
|
async logActivity(action, details = {}) {
|
|
return this.put('activity', {
|
|
action,
|
|
...details,
|
|
created_at: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
async getRecentActivity(limit = 50) {
|
|
const all = await this.getAll('activity');
|
|
return all
|
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
|
.slice(0, limit);
|
|
}
|
|
|
|
// === UI State ===
|
|
|
|
async saveUIState(key, value) {
|
|
return this.put('ui_state', { key, value });
|
|
}
|
|
|
|
async getUIState(key) {
|
|
const data = await this.get('ui_state', key);
|
|
return data?.value;
|
|
}
|
|
|
|
// === Stats ===
|
|
|
|
async getStats() {
|
|
const stores = ['projects', 'components', 'tokens', 'styles', 'activity', 'sync_queue', 'cache'];
|
|
const stats = {};
|
|
|
|
for (const store of stores) {
|
|
stats[store] = await this.count(store);
|
|
}
|
|
|
|
// Storage estimate
|
|
if (navigator.storage && navigator.storage.estimate) {
|
|
const estimate = await navigator.storage.estimate();
|
|
stats.storage_used_mb = Math.round(estimate.usage / (1024 * 1024) * 100) / 100;
|
|
stats.storage_quota_mb = Math.round(estimate.quota / (1024 * 1024));
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
// === Sync with Backend ===
|
|
|
|
async syncFromBackend(storeName, data, keyField = 'id') {
|
|
// Clear existing and bulk insert
|
|
await this.clear(storeName);
|
|
for (const item of data) {
|
|
await this.put(storeName, item);
|
|
}
|
|
return data.length;
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
const dssDB = new DssDB();
|
|
|
|
// Export
|
|
export { DssDB };
|
|
export default dssDB;
|