Files
dss/admin-ui/js/db/indexed-db.js
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
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
2025-12-09 18:45:48 -03:00

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;