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
This commit is contained in:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View File

@@ -0,0 +1,298 @@
/**
* @fileoverview Manages application-wide notifications.
* Handles persistence via IndexedDB, real-time updates via SSE, and state management.
*/
import dssDB from '../db/indexed-db.js';
const NOTIFICATION_STORE = 'notifications';
class NotificationService extends EventTarget {
constructor() {
super();
this.notifications = [];
this.unreadCount = 0;
this._eventSource = null;
this._initialized = false;
this._broadcastChannel = null;
}
async init() {
if (this._initialized) return;
this._initialized = true;
await this._loadFromStorage();
this._connectToEvents();
this._setupCrossTabSync();
}
/**
* Setup cross-tab synchronization via BroadcastChannel API
* When notifications are modified in another tab, reload them
*/
_setupCrossTabSync() {
try {
this._broadcastChannel = new BroadcastChannel('dss-notifications');
this._broadcastChannel.onmessage = (event) => {
if (event.data?.type === 'notifications-updated') {
console.log('[NotificationService] Notifications updated in another tab, reloading...');
this._loadFromStorage();
}
};
} catch (error) {
console.warn('[NotificationService] BroadcastChannel not available:', error);
}
}
/**
* Notify other tabs about notification changes
* @private
*/
_notifyOtherTabs() {
if (this._broadcastChannel) {
try {
this._broadcastChannel.postMessage({ type: 'notifications-updated' });
} catch (error) {
console.warn('[NotificationService] Failed to broadcast update:', error);
}
}
}
async _loadFromStorage() {
try {
this.notifications = await dssDB.getAll(NOTIFICATION_STORE) || [];
this._sortNotifications();
this._updateUnreadCount();
this.dispatchEvent(new CustomEvent('notifications-updated', {
detail: { notifications: this.notifications }
}));
} catch (error) {
console.error('Failed to load notifications from storage:', error);
this.notifications = [];
}
}
_connectToEvents() {
// Only connect if SSE endpoint exists and we have an auth token
try {
// Get access token from localStorage
const authTokens = localStorage.getItem('auth_tokens');
if (!authTokens) {
console.log('[NotificationService] No auth token available, skipping SSE connection');
return;
}
const { accessToken } = JSON.parse(authTokens);
if (!accessToken) {
console.log('[NotificationService] No access token found, skipping SSE connection');
return;
}
// Construct SSE URL with token parameter (EventSource can't send custom headers)
const sseUrl = `/api/notifications/events?token=${encodeURIComponent(accessToken)}`;
console.log('[NotificationService] Connecting to SSE with authentication...');
this._eventSource = new EventSource(sseUrl);
this._eventSource.onmessage = (event) => {
try {
const notificationData = JSON.parse(event.data);
console.log('[NotificationService] SSE notification received:', notificationData.type);
// From SSE, persist to local storage
this.create(notificationData, true);
} catch (error) {
console.error('[NotificationService] Error parsing SSE notification:', error);
}
};
this._eventSource.onerror = (err) => {
console.warn('[NotificationService] SSE connection error, using local-only mode');
this._eventSource.close();
this._eventSource = null;
};
this._eventSource.onopen = () => {
console.log('[NotificationService] ✅ SSE connection established successfully');
};
} catch (error) {
console.warn('[NotificationService] SSE setup error:', error);
}
}
_sortNotifications() {
this.notifications.sort((a, b) => b.timestamp - a.timestamp);
}
_updateUnreadCount() {
const newCount = this.notifications.filter(n => !n.read).length;
if (newCount !== this.unreadCount) {
this.unreadCount = newCount;
this.dispatchEvent(new CustomEvent('unread-count-changed', {
detail: { count: this.unreadCount }
}));
}
}
_dispatchUpdate() {
this._sortNotifications();
this._updateUnreadCount();
this.dispatchEvent(new CustomEvent('notifications-updated', {
detail: { notifications: this.notifications }
}));
}
/**
* Creates a new notification.
* @param {object} notificationData - The notification data.
* @param {string} notificationData.title - Notification title
* @param {string} [notificationData.message] - Notification message
* @param {string} [notificationData.type='info'] - 'info', 'success', 'warning', 'error'
* @param {string} [notificationData.source] - Source service name
* @param {Array} [notificationData.actions] - Action buttons
* @param {boolean} [persist=true] - Whether to save to IndexedDB.
* @returns {Promise<object>} The created notification.
*/
async create(notificationData, persist = true) {
const newNotification = {
id: crypto.randomUUID(),
timestamp: Date.now(),
read: false,
type: 'info',
...notificationData,
};
this.notifications.unshift(newNotification);
if (persist) {
try {
await dssDB.put(NOTIFICATION_STORE, newNotification);
} catch (error) {
console.error('Failed to persist notification:', error);
}
}
this._dispatchUpdate();
this._notifyOtherTabs();
// Also show as toast for immediate visibility
if (typeof window.showToast === 'function') {
window.showToast({
message: `<strong>${newNotification.title}</strong>${newNotification.message ? `<br>${newNotification.message}` : ''}`,
type: newNotification.type,
duration: 5000,
dismissible: true
});
}
return newNotification;
}
/**
* Retrieves all notifications.
* @returns {object[]}
*/
getAll() {
return [...this.notifications];
}
/**
* Get unread count
* @returns {number}
*/
getUnreadCount() {
return this.unreadCount;
}
/**
* Marks a specific notification as read.
* @param {string} id - The ID of the notification to update.
* @returns {Promise<void>}
*/
async markAsRead(id) {
const notification = this.notifications.find(n => n.id === id);
if (notification && !notification.read) {
notification.read = true;
try {
await dssDB.put(NOTIFICATION_STORE, notification);
} catch (error) {
console.error('Failed to update notification:', error);
}
this._dispatchUpdate();
this._notifyOtherTabs();
}
}
/**
* Marks all unread notifications as read.
* @returns {Promise<void>}
*/
async markAllAsRead() {
const updates = [];
this.notifications.forEach(n => {
if (!n.read) {
n.read = true;
updates.push(dssDB.put(NOTIFICATION_STORE, n));
}
});
if (updates.length > 0) {
try {
await Promise.all(updates);
} catch (error) {
console.error('Failed to mark all as read:', error);
}
this._dispatchUpdate();
this._notifyOtherTabs();
}
}
/**
* Deletes a notification by its ID.
* @param {string} id
* @returns {Promise<void>}
*/
async delete(id) {
this.notifications = this.notifications.filter(n => n.id !== id);
try {
await dssDB.delete(NOTIFICATION_STORE, id);
} catch (error) {
console.error('Failed to delete notification:', error);
}
this._dispatchUpdate();
this._notifyOtherTabs();
}
/**
* Clears all notifications.
* @returns {Promise<void>}
*/
async clearAll() {
this.notifications = [];
try {
await dssDB.clear(NOTIFICATION_STORE);
} catch (error) {
console.error('Failed to clear notifications:', error);
}
this._dispatchUpdate();
this._notifyOtherTabs();
}
/**
* Cleanup on app shutdown
*/
destroy() {
if (this._eventSource) {
this._eventSource.close();
this._eventSource = null;
}
if (this._broadcastChannel) {
this._broadcastChannel.close();
this._broadcastChannel = null;
}
}
}
// Export a singleton instance
const notificationService = new NotificationService();
export default notificationService;