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

449
admin-ui/js/core/router.js Normal file
View File

@@ -0,0 +1,449 @@
/**
* DSS Router
*
* Centralized hash-based routing with guards, lifecycle hooks, and
* declarative route definitions for enterprise-grade navigation management.
*
* @module router
*/
import { notifyError, ErrorCode } from './messaging.js';
/**
* Route configuration object
* @typedef {Object} RouteConfig
* @property {string} path - Route path (e.g., '/dashboard', '/projects')
* @property {string} name - Route name for programmatic navigation
* @property {Function} handler - Route handler function
* @property {Function} [beforeEnter] - Guard called before entering route
* @property {Function} [afterEnter] - Hook called after entering route
* @property {Function} [onLeave] - Hook called when leaving route
* @property {Object} [meta] - Route metadata
*/
/**
* Router class for centralized route management
*/
class Router {
constructor() {
this.routes = new Map();
this.currentRoute = null;
this.previousRoute = null;
this.defaultRoute = 'projects'; // Updated default for new architecture
this.isNavigating = false;
// Bind handlers
this.handleHashChange = this.handleHashChange.bind(this);
this.handlePopState = this.handlePopState.bind(this);
}
/**
* Initialize the router
*/
init() {
// Register new feature module routes
this.registerAll([
{
path: '/projects',
name: 'Projects',
handler: () => this.loadModule('dss-projects-module', () => import('../modules/projects/ProjectsModule.js'))
},
{
path: '/config',
name: 'Configuration',
handler: () => this.loadModule('dss-config-module', () => import('../modules/config/ConfigModule.js'))
},
{
path: '/components',
name: 'Components',
handler: () => this.loadModule('dss-components-module', () => import('../modules/components/ComponentsModule.js'))
},
{
path: '/translations',
name: 'Translations',
handler: () => this.loadModule('dss-translations-module', () => import('../modules/translations/TranslationsModule.js'))
},
{
path: '/discovery',
name: 'Discovery',
handler: () => this.loadModule('dss-discovery-module', () => import('../modules/discovery/DiscoveryModule.js'))
},
{
path: '/admin',
name: 'Admin',
handler: () => this.loadModule('dss-admin-module', () => import('../modules/admin/AdminModule.js'))
}
]);
// Listen for hash changes
window.addEventListener('hashchange', this.handleHashChange);
window.addEventListener('popstate', this.handlePopState);
// Handle initial route
this.handleHashChange();
}
/**
* Helper to load dynamic modules into the stage
* @param {string} tagName - Custom element tag name
* @param {Function} importFn - Dynamic import function
*/
async loadModule(tagName, importFn) {
try {
// 1. Load the module file
await importFn();
// 2. Update the stage content
const stageContent = document.querySelector('#stage-workdesk-content');
if (stageContent) {
stageContent.innerHTML = '';
const element = document.createElement(tagName);
stageContent.appendChild(element);
}
} catch (error) {
console.error(`Failed to load module ${tagName}:`, error);
notifyError(`Failed to load module`, ErrorCode.SYSTEM_UNEXPECTED);
}
}
/**
* Register a route
* @param {RouteConfig} config - Route configuration
*/
register(config) {
if (!config.path) {
throw new Error('Route path is required');
}
if (!config.handler) {
throw new Error('Route handler is required');
}
// Normalize path (remove leading slash for hash routing)
const path = config.path.replace(/^\//, '');
this.routes.set(path, {
path,
name: config.name || path,
handler: config.handler,
beforeEnter: config.beforeEnter || null,
afterEnter: config.afterEnter || null,
onLeave: config.onLeave || null,
meta: config.meta || {},
});
return this;
}
/**
* Register multiple routes
* @param {RouteConfig[]} routes - Array of route configurations
*/
registerAll(routes) {
routes.forEach(route => this.register(route));
return this;
}
/**
* Set default route
* @param {string} path - Default route path
*/
setDefaultRoute(path) {
this.defaultRoute = path.replace(/^\//, '');
return this;
}
/**
* Navigate to a route
* @param {string} path - Route path or name
* @param {Object} [options] - Navigation options
* @param {boolean} [options.replace] - Replace history instead of push
* @param {Object} [options.state] - State to pass to route
*/
navigate(path, options = {}) {
const normalizedPath = path.replace(/^\//, '');
// Update hash
if (options.replace) {
window.location.replace(`#${normalizedPath}`);
} else {
window.location.hash = normalizedPath;
}
return this;
}
/**
* Navigate to a route by name
* @param {string} name - Route name
* @param {Object} [options] - Navigation options
*/
navigateByName(name, options = {}) {
const route = Array.from(this.routes.values()).find(r => r.name === name);
if (!route) {
notifyError(`Route not found: ${name}`, ErrorCode.SYSTEM_UNEXPECTED);
return this;
}
return this.navigate(route.path, options);
}
/**
* Handle hash change event
*/
async handleHashChange() {
if (this.isNavigating) return;
this.isNavigating = true;
try {
// Get path from hash
let path = window.location.hash.replace(/^#/, '') || this.defaultRoute;
// Extract route and params
const { routePath, params } = this.parseRoute(path);
// Find route
const route = this.routes.get(routePath);
if (!route) {
console.warn(`Route not registered: ${routePath}, falling back to default`);
this.navigate(this.defaultRoute, { replace: true });
return;
}
// Call onLeave hook for previous route
if (this.currentRoute && this.currentRoute.onLeave) {
await this.callHook(this.currentRoute.onLeave, { from: this.currentRoute, to: route });
}
// Call beforeEnter guard
if (route.beforeEnter) {
const canEnter = await this.callGuard(route.beforeEnter, { route, params });
if (!canEnter) {
// Guard rejected, stay on current route or go to default
if (this.currentRoute) {
this.navigate(this.currentRoute.path, { replace: true });
} else {
this.navigate(this.defaultRoute, { replace: true });
}
return;
}
}
// Update route tracking
this.previousRoute = this.currentRoute;
this.currentRoute = route;
// Call route handler
await route.handler({ route, params, router: this });
// Call afterEnter hook
if (route.afterEnter) {
await this.callHook(route.afterEnter, { route, params, from: this.previousRoute });
}
// Emit route change event
this.emitRouteChange(route, params);
} catch (error) {
console.error('Router navigation error:', error);
notifyError('Navigation failed', ErrorCode.SYSTEM_UNEXPECTED, {
path: window.location.hash,
error: error.message,
});
} finally {
this.isNavigating = false;
}
}
/**
* Handle popstate event (browser back/forward)
*/
handlePopState(event) {
this.handleHashChange();
}
/**
* Parse route path and extract params
* @param {string} path - Route path
* @returns {Object} Route path and params
*/
parseRoute(path) {
// For now, simple implementation - just return the path
// Can be extended to support params like '/projects/:id'
const [routePath, queryString] = path.split('?');
const params = {};
if (queryString) {
new URLSearchParams(queryString).forEach((value, key) => {
params[key] = value;
});
}
return { routePath, params };
}
/**
* Call a route guard
* @param {Function} guard - Guard function
* @param {Object} context - Guard context
* @returns {Promise<boolean>} Whether navigation should proceed
*/
async callGuard(guard, context) {
try {
const result = await guard(context);
return result !== false; // Undefined or true = proceed
} catch (error) {
console.error('Route guard error:', error);
return false;
}
}
/**
* Call a lifecycle hook
* @param {Function} hook - Hook function
* @param {Object} context - Hook context
*/
async callHook(hook, context) {
try {
await hook(context);
} catch (error) {
console.error('Route hook error:', error);
}
}
/**
* Emit route change event
* @param {Object} route - Current route
* @param {Object} params - Route params
*/
emitRouteChange(route, params) {
window.dispatchEvent(new CustomEvent('route-changed', {
detail: {
route,
params,
previous: this.previousRoute,
}
}));
}
/**
* Get current route
* @returns {Object|null} Current route config
*/
getCurrentRoute() {
return this.currentRoute;
}
/**
* Get previous route
* @returns {Object|null} Previous route config
*/
getPreviousRoute() {
return this.previousRoute;
}
/**
* Check if a route exists
* @param {string} path - Route path
* @returns {boolean} Whether route exists
*/
hasRoute(path) {
const normalizedPath = path.replace(/^\//, '');
return this.routes.has(normalizedPath);
}
/**
* Get route by path
* @param {string} path - Route path
* @returns {Object|null} Route config
*/
getRoute(path) {
const normalizedPath = path.replace(/^\//, '');
return this.routes.get(normalizedPath) || null;
}
/**
* Get all routes
* @returns {Array} All registered routes
*/
getAllRoutes() {
return Array.from(this.routes.values());
}
/**
* Go back in history
*/
back() {
window.history.back();
return this;
}
/**
* Go forward in history
*/
forward() {
window.history.forward();
return this;
}
/**
* Destroy the router (cleanup)
*/
destroy() {
window.removeEventListener('hashchange', this.handleHashChange);
window.removeEventListener('popstate', this.handlePopState);
this.routes.clear();
this.currentRoute = null;
this.previousRoute = null;
}
}
// Singleton instance
const router = new Router();
// Export both the instance and the class
export { Router };
export default router;
/**
* Common route guards
*/
export const guards = {
/**
* Require authentication
*/
requireAuth({ route }) {
// Check if user is authenticated
// For now, always allow (implement auth later)
return true;
},
/**
* Require specific permission
*/
requirePermission(permission) {
return ({ route }) => {
// Check if user has permission
// For now, always allow (implement RBAC later)
return true;
};
},
/**
* Require project selected
*/
requireProject({ route, params }) {
const projectId = localStorage.getItem('dss_selected_project');
if (!projectId) {
notifyError('Please select a project first', ErrorCode.USER_ACTION_FORBIDDEN);
return false;
}
return true;
},
};