Fix frontend errors and pydantic config for local development

Frontend fixes:
- Copy missing JS files from blackroad-os/ to backend/static/js/
  - os.js (core OS functionality)
  - components.js (UI components)
  - registry.js (app registry)
  - app.js, config.js, theme.js, mock_data.js (supporting files)
- Fixes 3 ERROR findings from Cece audit
- System health: 0 ERRORS → 94 SUCCESSES (from 91)

Backend config fix:
- Add `extra = "ignore"` to Settings.Config in backend/app/config.py
- Allows .env.example to have more vars than Settings class defines
- Fixes Pydantic v2 validation errors on startup
- Enables local development without removing env template vars

Cece audit results after fixes:
🔴 CRITICAL: 0
🟠 ERROR:    0 (was 3)
🟡 WARNING:  6
🟢 SUCCESS:  94 (was 91)
This commit is contained in:
Claude
2025-11-20 01:38:56 +00:00
parent f9088d7b8b
commit a180873b7d
8 changed files with 2878 additions and 0 deletions

View File

@@ -63,6 +63,7 @@ class Settings(BaseSettings):
class Config: class Config:
env_file = ".env" env_file = ".env"
case_sensitive = True case_sensitive = True
extra = "ignore" # Allow extra env vars in .env file
settings = Settings() settings = Settings()

391
backend/static/js/app.js Normal file
View File

@@ -0,0 +1,391 @@
/**
* BlackRoad OS Bootloader
* Initializes the desktop environment and starts core services
*
* Responsibilities:
* - Render desktop icons from app registry
* - Populate start menu
* - Setup system tray interactions
* - Start system clock
* - Register global keyboard shortcuts
* - Display welcome notification
* - Wire up desktop-level event listeners
*
* Boot Sequence:
* 1. OS core (os.js) initializes
* 2. Theme manager (theme.js) initializes
* 3. Apps register themselves (apps/*.js)
* 4. Registry builds app manifest (registry.js)
* 5. Bootloader renders desktop (this file)
*
* This file is loaded LAST to ensure all dependencies are available
*/
class BootLoader {
constructor() {
// DOM references
this.desktopIcons = document.getElementById('desktop-icons');
this.startButton = document.getElementById('start-button');
this.startMenu = document.getElementById('start-menu');
this.systemClock = document.getElementById('system-clock');
// Keyboard shortcut registry (centralized for maintainability)
this.shortcuts = [
{ key: 'P', ctrl: true, shift: true, app: 'prism', description: 'Open Prism Console' },
{ key: 'M', ctrl: true, shift: true, app: 'miners', description: 'Open Miners Dashboard' },
{ key: 'E', ctrl: true, shift: true, app: 'engineering', description: 'Open Engineering DevTools' }
// TODO v0.2.0: Make shortcuts customizable via Settings app
];
this.boot();
}
boot() {
console.log('🚀 Booting BlackRoad OS...');
// Render desktop environment
this.renderDesktopIcons();
this.populateStartMenu();
// Setup interactions
this.setupStartMenu();
this.setupSystemTray();
// Start services
this.startClock();
// Register keyboard shortcuts
this.registerKeyboardShortcuts();
// Setup event listeners
this.setupEventListeners();
// Show welcome notification
this.showWelcome();
console.log('✅ BlackRoad OS ready');
window.OS.eventBus.emit('os:ready', { timestamp: new Date().toISOString() });
}
/**
* Render desktop icons from app registry
* Double-click to launch apps
*/
renderDesktopIcons() {
const apps = Object.values(AppRegistry);
apps.forEach(app => {
const icon = document.createElement('div');
icon.className = 'desktop-icon';
icon.dataset.appId = app.id;
icon.setAttribute('role', 'button');
icon.setAttribute('tabindex', '0');
icon.setAttribute('aria-label', `Launch ${app.name}`);
const iconImage = document.createElement('div');
iconImage.className = 'desktop-icon-image';
iconImage.textContent = app.icon;
iconImage.setAttribute('aria-hidden', 'true');
const iconLabel = document.createElement('div');
iconLabel.className = 'desktop-icon-label';
iconLabel.textContent = app.name;
icon.appendChild(iconImage);
icon.appendChild(iconLabel);
// Double-click to launch (mouse)
icon.addEventListener('dblclick', () => {
launchApp(app.id);
});
// Enter or Space to launch (keyboard)
icon.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
launchApp(app.id);
}
});
this.desktopIcons.appendChild(icon);
});
console.log(`🖥️ Rendered ${apps.length} desktop icons`);
}
/**
* Populate start menu with all apps
* Click to launch and close menu
*/
populateStartMenu() {
const menuApps = document.getElementById('start-menu-apps');
const apps = Object.values(AppRegistry);
apps.forEach((app, index) => {
const item = document.createElement('div');
item.className = 'start-menu-item';
item.setAttribute('role', 'menuitem');
item.setAttribute('tabindex', '0');
const icon = document.createElement('div');
icon.className = 'start-menu-item-icon';
icon.textContent = app.icon;
icon.setAttribute('aria-hidden', 'true');
const details = document.createElement('div');
details.className = 'start-menu-item-details';
const name = document.createElement('div');
name.className = 'start-menu-item-name';
name.textContent = app.name;
const desc = document.createElement('div');
desc.className = 'start-menu-item-desc';
desc.textContent = app.description;
details.appendChild(name);
details.appendChild(desc);
item.appendChild(icon);
item.appendChild(details);
// Click to launch
item.addEventListener('click', () => {
launchApp(app.id);
this.startMenu.style.display = 'none';
});
// Keyboard navigation
item.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
launchApp(app.id);
this.startMenu.style.display = 'none';
}
// Arrow key navigation within start menu
if (e.key === 'ArrowDown') {
e.preventDefault();
const next = item.nextElementSibling;
if (next && next.classList.contains('start-menu-item')) {
next.focus();
}
}
if (e.key === 'ArrowUp') {
e.preventDefault();
const prev = item.previousElementSibling;
if (prev && prev.classList.contains('start-menu-item')) {
prev.focus();
}
}
// Escape to close menu
if (e.key === 'Escape') {
this.startMenu.style.display = 'none';
this.startButton.focus();
}
});
menuApps.appendChild(item);
});
console.log(`📋 Populated start menu with ${apps.length} apps`);
}
/**
* Setup start menu toggle and interactions
*/
setupStartMenu() {
// Toggle start menu on button click
this.startButton.addEventListener('click', (e) => {
e.stopPropagation();
const isVisible = this.startMenu.style.display === 'block';
this.startMenu.style.display = isVisible ? 'none' : 'block';
// Focus first menu item when opening
if (!isVisible) {
const firstItem = this.startMenu.querySelector('.start-menu-item');
if (firstItem) {
setTimeout(() => firstItem.focus(), 100);
}
}
});
// Keyboard support for start button
this.startButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.startButton.click();
}
});
// Close start menu when clicking outside
document.addEventListener('click', (e) => {
if (!this.startMenu.contains(e.target) && !this.startButton.contains(e.target)) {
this.startMenu.style.display = 'none';
}
});
// Escape to close start menu
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.startMenu.style.display === 'block') {
this.startMenu.style.display = 'none';
this.startButton.focus();
}
});
// Shutdown button
const shutdownBtn = document.getElementById('shutdown-btn');
shutdownBtn.addEventListener('click', () => {
if (confirm('Are you sure you want to shutdown BlackRoad OS?')) {
window.OS.showNotification({
type: 'info',
title: 'Shutting Down',
message: 'BlackRoad OS is shutting down...',
duration: 2000
});
setTimeout(() => {
// Reload the page to simulate shutdown/restart
window.location.reload();
}, 2000);
}
});
}
/**
* Setup system tray icon interactions
*/
setupSystemTray() {
// Notifications tray icon
const notificationsTray = document.getElementById('notifications-tray');
notificationsTray.addEventListener('click', () => {
launchApp('notifications');
});
// Settings tray icon
const settingsTray = document.getElementById('settings-tray');
settingsTray.addEventListener('click', () => {
launchApp('settings');
});
// Update notification badge count
this.updateNotificationBadge();
// Update badge periodically (in real app, would listen to events)
setInterval(() => this.updateNotificationBadge(), 30000);
}
/**
* Update notification badge count
*/
updateNotificationBadge() {
const badge = document.getElementById('notification-badge');
const unreadCount = MockData.notifications.filter(n => !n.read).length;
if (unreadCount > 0) {
badge.textContent = unreadCount > 99 ? '99+' : unreadCount;
badge.style.display = 'block';
badge.setAttribute('aria-label', `${unreadCount} unread notifications`);
} else {
badge.style.display = 'none';
}
}
/**
* Start system clock (updates every second)
*/
startClock() {
const updateClock = () => {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const timeString = `${hours}:${minutes}`;
this.systemClock.textContent = timeString;
this.systemClock.setAttribute('aria-label', `Current time: ${timeString}`);
};
updateClock();
setInterval(updateClock, 1000);
console.log('🕐 System clock started');
}
/**
* Register global keyboard shortcuts
*/
registerKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Check each registered shortcut
this.shortcuts.forEach(shortcut => {
const ctrlMatch = shortcut.ctrl ? e.ctrlKey : !e.ctrlKey;
const shiftMatch = shortcut.shift ? e.shiftKey : !e.shiftKey;
const altMatch = shortcut.alt ? e.altKey : !e.altKey;
const keyMatch = e.key.toUpperCase() === shortcut.key.toUpperCase();
if (ctrlMatch && shiftMatch && altMatch && keyMatch) {
e.preventDefault();
launchApp(shortcut.app);
}
});
});
console.log(`⌨️ Registered ${this.shortcuts.length} keyboard shortcuts`);
}
/**
* Show welcome notification on boot
*/
showWelcome() {
setTimeout(() => {
window.OS.showNotification({
type: 'success',
title: 'Welcome to BlackRoad OS',
message: 'System initialized successfully. All services online.',
duration: 5000
});
}, 500);
}
/**
* Setup desktop-level event listeners
*/
setupEventListeners() {
// Listen for window lifecycle events
window.OS.eventBus.on('window:created', (data) => {
console.log(`🪟 Window created: ${data.windowId}`);
});
window.OS.eventBus.on('window:closed', (data) => {
console.log(`❌ Window closed: ${data.windowId}`);
});
window.OS.eventBus.on('theme:changed', (data) => {
console.log(`🎨 Theme changed: ${data.theme}`);
});
// Listen for notification badge updates
// In a real app, would listen to notification events from backend
window.OS.eventBus.on('notification:shown', () => {
// Could update badge here if notifications came from apps
});
}
/**
* Get list of registered shortcuts (for Settings or Help)
* @returns {Array} Shortcut definitions
*/
getShortcuts() {
return this.shortcuts;
}
}
// Boot when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.BootLoader = new BootLoader();
});
} else {
window.BootLoader = new BootLoader();
}

View File

@@ -0,0 +1,764 @@
/**
* BlackRoad OS Component Library
* Reusable UI primitives for building accessible app interfaces
*
* Philosophy:
* - Vanilla JS only (no framework dependencies)
* - Accessibility-first (ARIA attributes, keyboard navigation)
* - Predictable API (consistent naming and options)
* - Composable (components can contain other components)
* - Theme-aware (uses CSS variables from styles.css)
*
* Usage:
* const card = Components.Card({ title: 'My Card', content: '...' });
* document.body.appendChild(card);
*
* All components return HTMLElement that can be:
* - Appended to DOM
* - Passed to window.OS.createWindow({ content: ... })
* - Composed with other components
*/
const Components = {
/**
* Create a Card component
* A container with optional header, body, and footer sections
*
* @param {Object} options - Card configuration
* @param {string} [options.title] - Card title
* @param {string} [options.subtitle] - Card subtitle
* @param {HTMLElement|string} [options.content] - Card body content
* @param {HTMLElement|string} [options.footer] - Card footer content
* @param {HTMLElement} [options.headerActions] - Action buttons for header
* @returns {HTMLElement} Card element
*
* @example
* const card = Components.Card({
* title: 'System Status',
* subtitle: 'Last updated: 2 minutes ago',
* content: 'All systems operational'
* });
*/
Card(options = {}) {
const card = document.createElement('div');
card.className = 'card';
card.setAttribute('role', 'article');
if (options.title) {
const header = document.createElement('div');
header.className = 'card-header';
const titleDiv = document.createElement('div');
const title = document.createElement('div');
title.className = 'card-title';
title.textContent = options.title;
titleDiv.appendChild(title);
if (options.subtitle) {
const subtitle = document.createElement('div');
subtitle.className = 'card-subtitle';
subtitle.textContent = options.subtitle;
titleDiv.appendChild(subtitle);
}
header.appendChild(titleDiv);
if (options.headerActions) {
const actionsWrapper = document.createElement('div');
actionsWrapper.className = 'card-header-actions';
actionsWrapper.appendChild(options.headerActions);
header.appendChild(actionsWrapper);
}
card.appendChild(header);
}
if (options.content) {
const body = document.createElement('div');
body.className = 'card-body';
if (typeof options.content === 'string') {
body.innerHTML = options.content;
} else if (options.content instanceof HTMLElement) {
body.appendChild(options.content);
}
card.appendChild(body);
}
if (options.footer) {
const footer = document.createElement('div');
footer.className = 'card-footer';
if (typeof options.footer === 'string') {
footer.innerHTML = options.footer;
} else if (options.footer instanceof HTMLElement) {
footer.appendChild(options.footer);
}
card.appendChild(footer);
}
return card;
},
/**
* Create a Badge component
* Small status indicator with color coding
*
* @param {string} text - Badge text
* @param {string} [type='neutral'] - Badge type (success, warning, error, info, neutral)
* @returns {HTMLElement} Badge element
*
* @example
* const badge = Components.Badge('Online', 'success');
*/
Badge(text, type = 'neutral') {
const validTypes = ['success', 'warning', 'error', 'info', 'neutral'];
const safeType = validTypes.includes(type) ? type : 'neutral';
const badge = document.createElement('span');
badge.className = `badge ${safeType}`;
badge.textContent = text;
badge.setAttribute('role', 'status');
badge.setAttribute('aria-label', `Status: ${text}`);
return badge;
},
/**
* Create a Table component
* Accessible data table with automatic header/body structure
*
* @param {Array<Object>} columns - Column definitions [{ key, label, render }]
* @param {Array<Object>} data - Row data objects
* @param {Object} [options] - Table options
* @param {string} [options.caption] - Table caption for accessibility
* @param {boolean} [options.striped=false] - Alternate row colors
* @returns {HTMLElement} Table container element
*
* @example
* const table = Components.Table(
* [{ key: 'name', label: 'Name' }, { key: 'status', label: 'Status' }],
* [{ name: 'Alice', status: 'Active' }, { name: 'Bob', status: 'Inactive' }],
* { caption: 'User list' }
* );
*/
Table(columns, data, options = {}) {
const container = document.createElement('div');
container.className = 'table-container';
const table = document.createElement('table');
table.className = 'table';
if (options.striped) {
table.classList.add('table-striped');
}
// Add caption for accessibility
if (options.caption) {
const caption = document.createElement('caption');
caption.textContent = options.caption;
caption.className = 'sr-only'; // Screen reader only
table.appendChild(caption);
}
// Header
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
columns.forEach(col => {
const th = document.createElement('th');
th.textContent = col.label;
th.setAttribute('scope', 'col');
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Body
const tbody = document.createElement('tbody');
if (data.length === 0) {
const emptyRow = document.createElement('tr');
const emptyCell = document.createElement('td');
emptyCell.setAttribute('colspan', columns.length);
emptyCell.className = 'table-empty';
emptyCell.textContent = 'No data available';
emptyRow.appendChild(emptyCell);
tbody.appendChild(emptyRow);
} else {
data.forEach(row => {
const tr = document.createElement('tr');
columns.forEach(col => {
const td = document.createElement('td');
const value = row[col.key];
// Support custom render function
if (col.render && typeof col.render === 'function') {
const rendered = col.render(value, row);
if (rendered instanceof HTMLElement) {
td.appendChild(rendered);
} else {
td.innerHTML = rendered;
}
} else if (value instanceof HTMLElement) {
td.appendChild(value);
} else {
td.innerHTML = value !== undefined && value !== null ? value : '-';
}
tr.appendChild(td);
});
tbody.appendChild(tr);
});
}
table.appendChild(tbody);
container.appendChild(table);
return container;
},
/**
* Create a List component
* Accessible list with icon, title, subtitle, and actions
*
* @param {Array<Object>} items - List items
* @param {string} [items[].icon] - Icon HTML or emoji
* @param {string} items[].title - Item title
* @param {string} [items[].subtitle] - Item subtitle
* @param {HTMLElement} [items[].actions] - Action buttons
* @param {Function} [items[].onClick] - Click handler
* @returns {HTMLElement} List element
*
* @example
* const list = Components.List([
* { icon: '📁', title: 'Documents', subtitle: '45 files' },
* { icon: '🖼️', title: 'Images', subtitle: '128 files' }
* ]);
*/
List(items) {
const list = document.createElement('ul');
list.className = 'list';
list.setAttribute('role', 'list');
items.forEach(item => {
const li = document.createElement('li');
li.className = 'list-item';
li.setAttribute('role', 'listitem');
if (item.icon) {
const icon = document.createElement('div');
icon.className = 'list-item-icon';
icon.innerHTML = item.icon;
icon.setAttribute('aria-hidden', 'true');
li.appendChild(icon);
}
const content = document.createElement('div');
content.className = 'list-item-content';
const title = document.createElement('div');
title.className = 'list-item-title';
title.textContent = item.title;
content.appendChild(title);
if (item.subtitle) {
const subtitle = document.createElement('div');
subtitle.className = 'list-item-subtitle';
subtitle.textContent = item.subtitle;
content.appendChild(subtitle);
}
li.appendChild(content);
if (item.actions) {
const actions = document.createElement('div');
actions.className = 'list-item-actions';
actions.appendChild(item.actions);
li.appendChild(actions);
}
if (item.onClick) {
li.classList.add('list-item-clickable');
li.setAttribute('role', 'button');
li.setAttribute('tabindex', '0');
li.addEventListener('click', item.onClick);
// Keyboard support
li.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
item.onClick(e);
}
});
}
list.appendChild(li);
});
return list;
},
/**
* Create a Stats Box component
* Display a metric with optional change indicator
*
* @param {Object} options - Stats box configuration
* @param {string|number} options.value - Main metric value
* @param {string} options.label - Metric label
* @param {number} [options.change] - Percent change (positive or negative)
* @param {string} [options.icon] - Optional icon
* @returns {HTMLElement} Stats box element
*
* @example
* const stats = Components.StatsBox({
* value: '42',
* label: 'Active Users',
* change: 12.5
* });
*/
StatsBox(options) {
const box = document.createElement('div');
box.className = 'stats-box';
box.setAttribute('role', 'figure');
box.setAttribute('aria-label', `${options.label}: ${options.value}`);
if (options.icon) {
const icon = document.createElement('div');
icon.className = 'stats-icon';
icon.innerHTML = options.icon;
icon.setAttribute('aria-hidden', 'true');
box.appendChild(icon);
}
const value = document.createElement('div');
value.className = 'stats-value';
value.textContent = options.value;
box.appendChild(value);
const label = document.createElement('div');
label.className = 'stats-label';
label.textContent = options.label;
box.appendChild(label);
if (options.change !== undefined) {
const change = document.createElement('div');
change.className = `stats-change ${options.change >= 0 ? 'positive' : 'negative'}`;
change.textContent = `${options.change >= 0 ? '+' : ''}${options.change}%`;
change.setAttribute('aria-label', `${options.change >= 0 ? 'Up' : 'Down'} ${Math.abs(options.change)} percent`);
box.appendChild(change);
}
return box;
},
/**
* Create a responsive Grid container
*
* @param {number|string} columns - Number of columns (2, 3, 4, 'auto')
* @param {Array<HTMLElement>} children - Child elements
* @returns {HTMLElement} Grid container
*
* @example
* const grid = Components.Grid(3, [card1, card2, card3]);
*/
Grid(columns, children) {
const grid = document.createElement('div');
grid.className = `grid grid-${columns}`;
grid.setAttribute('role', 'group');
children.forEach(child => {
if (child instanceof HTMLElement) {
grid.appendChild(child);
}
});
return grid;
},
/**
* Create a Graph/Chart placeholder
* For indicating where charts will be rendered (e.g., with Chart.js)
*
* @param {string} [label='Chart Visualization'] - Placeholder text
* @returns {HTMLElement} Placeholder element
*/
GraphPlaceholder(label = 'Chart Visualization') {
const placeholder = document.createElement('div');
placeholder.className = 'graph-placeholder';
placeholder.textContent = label;
placeholder.setAttribute('role', 'img');
placeholder.setAttribute('aria-label', `Placeholder for ${label}`);
return placeholder;
},
/**
* Create a Button component
*
* @param {string} text - Button text
* @param {Object} [options] - Button options
* @param {string} [options.type] - Button type (primary, danger, secondary)
* @param {string} [options.size] - Button size (small, medium, large)
* @param {Function} [options.onClick] - Click handler
* @param {boolean} [options.disabled=false] - Disabled state
* @param {string} [options.icon] - Optional icon
* @param {string} [options.ariaLabel] - Custom ARIA label
* @returns {HTMLElement} Button element
*
* @example
* const btn = Components.Button('Save', {
* type: 'primary',
* onClick: () => console.log('Saved!')
* });
*/
Button(text, options = {}) {
const btn = document.createElement('button');
btn.className = 'btn';
if (options.icon) {
const icon = document.createElement('span');
icon.className = 'btn-icon';
icon.innerHTML = options.icon;
icon.setAttribute('aria-hidden', 'true');
btn.appendChild(icon);
}
const textSpan = document.createElement('span');
textSpan.textContent = text;
btn.appendChild(textSpan);
if (options.type) {
btn.classList.add(options.type);
}
if (options.size) {
btn.classList.add(options.size);
}
if (options.disabled) {
btn.disabled = true;
}
if (options.onClick) {
btn.addEventListener('click', options.onClick);
}
if (options.ariaLabel) {
btn.setAttribute('aria-label', options.ariaLabel);
}
return btn;
},
/**
* Create an Empty State component
* Displayed when no data is available
*
* @param {Object} options - Empty state configuration
* @param {string} [options.icon] - Icon or emoji
* @param {string} [options.title] - Empty state title
* @param {string} [options.text] - Empty state description
* @param {HTMLElement} [options.action] - Optional action button
* @returns {HTMLElement} Empty state container
*
* @example
* const empty = Components.EmptyState({
* icon: '📭',
* title: 'No messages',
* text: 'Your inbox is empty'
* });
*/
EmptyState(options) {
const container = document.createElement('div');
container.className = 'empty-state';
container.setAttribute('role', 'status');
container.setAttribute('aria-live', 'polite');
if (options.icon) {
const icon = document.createElement('div');
icon.className = 'empty-state-icon';
icon.textContent = options.icon;
icon.setAttribute('aria-hidden', 'true');
container.appendChild(icon);
}
if (options.title) {
const title = document.createElement('div');
title.className = 'empty-state-title';
title.textContent = options.title;
container.appendChild(title);
}
if (options.text) {
const text = document.createElement('div');
text.className = 'empty-state-text';
text.textContent = options.text;
container.appendChild(text);
}
if (options.action) {
const actionWrapper = document.createElement('div');
actionWrapper.className = 'empty-state-action';
actionWrapper.appendChild(options.action);
container.appendChild(actionWrapper);
}
return container;
},
/**
* Create a Loading State component
* Displayed during async operations
*
* @param {string} [message='Loading...'] - Loading message
* @returns {HTMLElement} Loading state container
*
* @example
* const loading = Components.LoadingState('Fetching data...');
*/
LoadingState(message = 'Loading...') {
const container = document.createElement('div');
container.className = 'loading-state';
container.setAttribute('role', 'status');
container.setAttribute('aria-live', 'polite');
container.setAttribute('aria-busy', 'true');
const spinner = this.Spinner();
container.appendChild(spinner);
const text = document.createElement('div');
text.className = 'loading-state-text';
text.textContent = message;
container.appendChild(text);
return container;
},
/**
* Create an Error State component
* Displayed when operations fail
*
* @param {Object} options - Error state configuration
* @param {string} [options.title='Error'] - Error title
* @param {string} options.message - Error message
* @param {Function} [options.onRetry] - Retry callback
* @returns {HTMLElement} Error state container
*
* @example
* const error = Components.ErrorState({
* title: 'Failed to load',
* message: 'Could not connect to server',
* onRetry: () => fetchData()
* });
*/
ErrorState(options) {
const container = document.createElement('div');
container.className = 'error-state';
container.setAttribute('role', 'alert');
container.setAttribute('aria-live', 'assertive');
const icon = document.createElement('div');
icon.className = 'error-state-icon';
icon.textContent = '⚠️';
icon.setAttribute('aria-hidden', 'true');
container.appendChild(icon);
const title = document.createElement('div');
title.className = 'error-state-title';
title.textContent = options.title || 'Error';
container.appendChild(title);
const message = document.createElement('div');
message.className = 'error-state-message';
message.textContent = options.message;
container.appendChild(message);
if (options.onRetry) {
const retryBtn = this.Button('Retry', {
type: 'primary',
onClick: options.onRetry
});
container.appendChild(retryBtn);
}
return container;
},
/**
* Create a Loading Spinner
* Simple animated spinner for loading states
*
* @returns {HTMLElement} Spinner element
*/
Spinner() {
const spinner = document.createElement('div');
spinner.className = 'spinner';
spinner.setAttribute('role', 'progressbar');
spinner.setAttribute('aria-label', 'Loading');
spinner.setAttribute('aria-busy', 'true');
return spinner;
},
/**
* Create a Code Block
* Pre-formatted code display with syntax highlighting support
*
* @param {string} code - Code content
* @param {string} [language] - Programming language for syntax highlighting
* @returns {HTMLElement} Code block element
*
* @example
* const code = Components.CodeBlock('const x = 42;', 'javascript');
*/
CodeBlock(code, language) {
const block = document.createElement('pre');
block.className = 'code-block';
if (language) {
block.classList.add(`language-${language}`);
block.setAttribute('data-language', language);
}
const codeEl = document.createElement('code');
codeEl.textContent = code;
block.appendChild(codeEl);
return block;
},
/**
* Create a Sidebar Layout
* Two-column layout with sidebar and main content
*
* @param {HTMLElement} sidebar - Sidebar content
* @param {HTMLElement} content - Main content
* @param {Object} [options] - Layout options
* @param {string} [options.sidebarWidth='250px'] - Sidebar width
* @returns {HTMLElement} Layout container
*
* @example
* const layout = Components.SidebarLayout(menuEl, contentEl);
*/
SidebarLayout(sidebar, content, options = {}) {
const layout = document.createElement('div');
layout.className = 'sidebar-layout';
const sidebarEl = document.createElement('div');
sidebarEl.className = 'sidebar';
sidebarEl.setAttribute('role', 'complementary');
sidebarEl.setAttribute('aria-label', 'Sidebar navigation');
if (options.sidebarWidth) {
sidebarEl.style.width = options.sidebarWidth;
}
sidebarEl.appendChild(sidebar);
const contentEl = document.createElement('div');
contentEl.className = 'sidebar-content';
contentEl.setAttribute('role', 'main');
contentEl.appendChild(content);
layout.appendChild(sidebarEl);
layout.appendChild(contentEl);
return layout;
},
/**
* Create a Tabs component
* Tabbed interface with keyboard navigation
*
* @param {Array<Object>} tabs - Tab definitions
* @param {string} tabs[].id - Tab identifier
* @param {string} tabs[].label - Tab label
* @param {HTMLElement|string} tabs[].content - Tab content
* @returns {HTMLElement} Tabs container
*
* @example
* const tabs = Components.Tabs([
* { id: 'overview', label: 'Overview', content: overviewEl },
* { id: 'details', label: 'Details', content: detailsEl }
* ]);
*/
Tabs(tabs) {
const container = document.createElement('div');
container.className = 'tabs-container';
const tabsHeader = document.createElement('div');
tabsHeader.className = 'tabs';
tabsHeader.setAttribute('role', 'tablist');
const contentContainer = document.createElement('div');
contentContainer.className = 'tab-content';
tabs.forEach((tab, index) => {
const tabBtn = document.createElement('button');
tabBtn.className = 'tab';
tabBtn.textContent = tab.label;
tabBtn.setAttribute('role', 'tab');
tabBtn.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
tabBtn.setAttribute('aria-controls', `tab-panel-${tab.id}`);
tabBtn.setAttribute('id', `tab-${tab.id}`);
tabBtn.setAttribute('tabindex', index === 0 ? '0' : '-1');
if (index === 0) tabBtn.classList.add('active');
const tabContent = document.createElement('div');
tabContent.id = `tab-panel-${tab.id}`;
tabContent.className = 'tab-panel';
tabContent.setAttribute('role', 'tabpanel');
tabContent.setAttribute('aria-labelledby', `tab-${tab.id}`);
tabContent.setAttribute('tabindex', '0');
tabContent.style.display = index === 0 ? 'block' : 'none';
if (typeof tab.content === 'string') {
tabContent.innerHTML = tab.content;
} else if (tab.content instanceof HTMLElement) {
tabContent.appendChild(tab.content);
}
const activateTab = () => {
// Deactivate all tabs
tabsHeader.querySelectorAll('.tab').forEach(t => {
t.classList.remove('active');
t.setAttribute('aria-selected', 'false');
t.setAttribute('tabindex', '-1');
});
contentContainer.querySelectorAll('.tab-panel').forEach(c => {
c.style.display = 'none';
});
// Activate clicked tab
tabBtn.classList.add('active');
tabBtn.setAttribute('aria-selected', 'true');
tabBtn.setAttribute('tabindex', '0');
tabContent.style.display = 'block';
tabBtn.focus();
};
tabBtn.addEventListener('click', activateTab);
// Keyboard navigation
tabBtn.addEventListener('keydown', (e) => {
const tabButtons = Array.from(tabsHeader.querySelectorAll('.tab'));
const currentIndex = tabButtons.indexOf(tabBtn);
if (e.key === 'ArrowRight') {
e.preventDefault();
const nextIndex = (currentIndex + 1) % tabButtons.length;
tabButtons[nextIndex].click();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
const prevIndex = (currentIndex - 1 + tabButtons.length) % tabButtons.length;
tabButtons[prevIndex].click();
} else if (e.key === 'Home') {
e.preventDefault();
tabButtons[0].click();
} else if (e.key === 'End') {
e.preventDefault();
tabButtons[tabButtons.length - 1].click();
}
});
tabsHeader.appendChild(tabBtn);
contentContainer.appendChild(tabContent);
});
container.appendChild(tabsHeader);
container.appendChild(contentContainer);
return container;
}
};
// Make globally available
window.Components = Components;
// Log component library initialization
console.log('📦 Component library loaded:', Object.keys(Components).length, 'components');

258
backend/static/js/config.js Normal file
View File

@@ -0,0 +1,258 @@
/**
* BlackRoad OS Configuration
* Central configuration for API endpoints, feature flags, and app settings
*
* Purpose:
* - Provides single source of truth for environment-specific settings
* - Makes it easy to switch between mock and real APIs
* - Enables feature flag-based development
* - Centralizes app default configurations
*
* How to use:
* import { API_ENDPOINTS, FEATURE_FLAGS } from './config.js';
* const url = API_ENDPOINTS.prism + '/agents/runs';
*
* For real API integration (v0.2.0+):
* 1. Set FEATURE_FLAGS.enableRealAPIs = true
* 2. Update API_ENDPOINTS with actual backend URLs
* 3. Apps will automatically use real endpoints instead of MockData
*/
const Config = {
/**
* Application metadata
*/
APP: {
name: 'BlackRoad OS',
version: '0.1.1',
buildDate: '2025-11-16',
environment: 'development' // development | production
},
/**
* Feature flags for controlling functionality
* Toggle these to enable/disable features without code changes
*/
FEATURE_FLAGS: {
// API & Data
enableRealAPIs: false, // Use real backend APIs instead of mock data
enableDebugLogging: true, // Show detailed console logs
enablePerformanceMetrics: false, // Track and log performance metrics
// UI Features
enableCommandPalette: false, // Ctrl+K command palette (v0.2.0)
enableWindowResize: false, // Window resizing (v0.2.0)
enableWindowMaximize: false, // Window maximize (v0.2.0)
enableThemeBuilder: false, // Custom theme builder (v0.2.0)
enableNotificationSound: false, // Audio notifications
// Advanced Features
enableWindowPersistence: false, // Remember window positions (v0.2.0)
enableMultiUser: false, // Multi-user support (v0.3.0)
enableCollaboration: false, // Real-time collaboration (v0.3.0)
enableAnalytics: false // Usage analytics tracking
},
/**
* API Endpoints
* Update these when connecting to real backend services
*
* Current: All point to mock data layer
* Future: Point to actual FastAPI/Next.js backends
*/
API_ENDPOINTS: {
// Base URLs
base: 'https://api.blackroad.io', // Main API base
prism: 'https://api.blackroad.io/prism', // Prism agent system
miners: 'https://api.blackroad.io/miners', // Mining operations
piOps: 'https://api.blackroad.io/pi', // Pi device management
compliance: 'https://api.blackroad.io/compliance', // FINRA compliance
finance: 'https://api.blackroad.io/finance', // Portfolio/AUM
identity: 'https://api.blackroad.io/identity', // SHA∞ identity
research: 'https://api.blackroad.io/research', // Lucidia research
// Specific endpoints (examples for future use)
// TODO v0.2.0: Implement these when backend is ready
agents: {
runs: '/agents/runs',
status: '/agents/status',
logs: '/agents/logs'
},
miners: {
list: '/miners',
status: '/miners/:id/status',
telemetry: '/miners/:id/telemetry'
}
},
/**
* App default configurations
* Default window sizes, behaviors, and settings for each app
*/
APPS: {
prism: {
defaultWidth: '900px',
defaultHeight: '700px',
refreshInterval: 5000, // Auto-refresh every 5s
maxLogLines: 1000
},
miners: {
defaultWidth: '1000px',
defaultHeight: '700px',
refreshInterval: 10000, // Auto-refresh every 10s
alertThreshold: {
temperature: 80, // Alert if temp > 80°C
hashrate: 100 // Alert if hashrate < 100 TH/s
}
},
piOps: {
defaultWidth: '900px',
defaultHeight: '650px',
refreshInterval: 30000, // Auto-refresh every 30s
alertThreshold: {
cpu: 90, // Alert if CPU > 90%
memory: 90, // Alert if memory > 90%
disk: 95 // Alert if disk > 95%
}
},
runbooks: {
defaultWidth: '1100px',
defaultHeight: '750px',
enableMarkdownPreview: true,
autoSave: true
},
compliance: {
defaultWidth: '1000px',
defaultHeight: '700px',
refreshInterval: 60000, // Auto-refresh every minute
priorityLevels: ['critical', 'high', 'medium', 'low']
},
finance: {
defaultWidth: '1100px',
defaultHeight: '750px',
refreshInterval: 15000, // Auto-refresh every 15s
currency: 'USD',
chartType: 'line' // line | bar | candlestick
},
identity: {
defaultWidth: '1000px',
defaultHeight: '700px',
searchDebounce: 300, // Debounce search by 300ms
pageSize: 50
},
research: {
defaultWidth: '1000px',
defaultHeight: '700px',
enableLivePreview: true,
autoSaveInterval: 30000 // Auto-save every 30s
},
engineering: {
defaultWidth: '900px',
defaultHeight: '700px',
showInternalMetrics: true
},
settings: {
defaultWidth: '700px',
defaultHeight: '600px'
},
notifications: {
defaultWidth: '500px',
defaultHeight: '600px',
markReadOnOpen: true,
maxUnreadBadge: 99
},
corporate: {
defaultWidth: '800px',
defaultHeight: '600px'
}
},
/**
* Theme configuration
*/
THEME: {
default: 'tealOS',
available: ['tealOS', 'nightOS'],
transitionDuration: 300 // ms
},
/**
* System settings
*/
SYSTEM: {
notificationDuration: 5000, // Default notification duration (ms)
clockFormat: '24h', // 24h | 12h
dateFormat: 'YYYY-MM-DD', // ISO format
maxOpenWindows: 20, // Prevent memory issues
zIndexMax: 9999, // Z-index ceiling
autoSaveInterval: 60000 // Auto-save user data every minute
},
/**
* Keyboard shortcuts
* Centralized registry (can be overridden in Settings v0.2.0)
*/
SHORTCUTS: {
openCommandPalette: 'Ctrl+K',
openPrism: 'Ctrl+Shift+P',
openMiners: 'Ctrl+Shift+M',
openEngineering: 'Ctrl+Shift+E',
closeWindow: 'Escape',
toggleTheme: 'Ctrl+Shift+T', // TODO: Implement
toggleFullscreen: 'F11' // TODO: Implement
},
/**
* Get app configuration
* @param {string} appId - App identifier
* @returns {Object} App configuration
*/
getAppConfig(appId) {
return this.APPS[appId] || {};
},
/**
* Check if feature flag is enabled
* @param {string} flagName - Feature flag name
* @returns {boolean} True if enabled
*/
isFeatureEnabled(flagName) {
return this.FEATURE_FLAGS[flagName] === true;
},
/**
* Get API endpoint
* @param {string} service - Service name (e.g., 'prism', 'miners')
* @param {string} path - Optional path to append
* @returns {string} Full API URL
*/
getApiEndpoint(service, path = '') {
const base = this.API_ENDPOINTS[service] || this.API_ENDPOINTS.base;
return path ? `${base}${path}` : base;
},
/**
* Log configuration (for debugging)
*/
logConfig() {
if (this.FEATURE_FLAGS.enableDebugLogging) {
console.log('🔧 BlackRoad OS Configuration:', {
version: this.APP.version,
environment: this.APP.environment,
realAPIsEnabled: this.FEATURE_FLAGS.enableRealAPIs,
featureFlags: this.FEATURE_FLAGS
});
}
}
};
// Make globally available
window.Config = Config;
// Log configuration on load
Config.logConfig();
// Export for ES modules (future-proof)
if (typeof module !== 'undefined' && module.exports) {
module.exports = Config;
}

View File

@@ -0,0 +1,190 @@
/**
* Mock Data Generator
* Comprehensive fake dataset for all BlackRoad OS apps
* TODO: Replace with real API calls in production
*/
const MockData = {
// Prism Console - Agent Run Logs
agentRuns: [
{ id: 'run_001', agent: 'ComplianceAgent', status: 'success', timestamp: '2025-11-16 09:23:15', duration: '2.3s', message: 'FINRA review completed' },
{ id: 'run_002', agent: 'MinerHealthCheck', status: 'success', timestamp: '2025-11-16 09:22:00', duration: '1.1s', message: 'All miners operational' },
{ id: 'run_003', agent: 'DataSyncAgent', status: 'running', timestamp: '2025-11-16 09:21:45', duration: '15.2s', message: 'Syncing identity ledger...' },
{ id: 'run_004', agent: 'PortfolioRebalance', status: 'failed', timestamp: '2025-11-16 09:20:30', duration: '0.8s', message: 'API timeout exceeded' },
{ id: 'run_005', agent: 'PiMonitor', status: 'success', timestamp: '2025-11-16 09:19:12', duration: '0.5s', message: 'All Pi devices responding' },
{ id: 'run_006', agent: 'RunbookSync', status: 'success', timestamp: '2025-11-16 09:18:00', duration: '3.2s', message: 'Updated 5 runbooks' },
{ id: 'run_007', agent: 'AuditLogger', status: 'success', timestamp: '2025-11-16 09:17:30', duration: '1.4s', message: 'Logged 47 events' },
{ id: 'run_008', agent: 'ChainValidator', status: 'success', timestamp: '2025-11-16 09:16:15', duration: '5.7s', message: 'RoadChain validated' },
{ id: 'run_009', agent: 'BackupAgent', status: 'success', timestamp: '2025-11-16 09:15:00', duration: '12.3s', message: 'Backup completed' },
{ id: 'run_010', agent: 'AlertProcessor', status: 'success', timestamp: '2025-11-16 09:14:22', duration: '0.3s', message: 'Processed 3 alerts' }
],
// Miners Dashboard - Mining Operations
miners: [
{ id: 'miner_01', name: 'BlackRoad-Alpha', status: 'online', hashrate: '450 TH/s', temp: 62, power: 3200, uptime: '45d 12h', location: 'Datacenter A' },
{ id: 'miner_02', name: 'BlackRoad-Beta', status: 'online', hashrate: '430 TH/s', temp: 58, power: 3100, uptime: '45d 11h', location: 'Datacenter A' },
{ id: 'miner_03', name: 'BlackRoad-Gamma', status: 'online', hashrate: '465 TH/s', temp: 64, power: 3250, uptime: '38d 6h', location: 'Datacenter B' },
{ id: 'miner_04', name: 'BlackRoad-Delta', status: 'offline', hashrate: '0 TH/s', temp: 0, power: 0, uptime: '0d 0h', location: 'Datacenter B' },
{ id: 'miner_05', name: 'BlackRoad-Epsilon', status: 'online', hashrate: '455 TH/s', temp: 61, power: 3180, uptime: '22d 3h', location: 'Datacenter C' },
{ id: 'miner_06', name: 'BlackRoad-Zeta', status: 'warning', hashrate: '380 TH/s', temp: 71, power: 3050, uptime: '15d 18h', location: 'Datacenter C' }
],
// Pi Ops - Raspberry Pi Devices
piDevices: [
{ id: 'pi_001', hostname: 'pi-gate-01', ip: '192.168.1.101', status: 'online', cpu: 23, memory: 45, disk: 38, uptime: '89d 14h', role: 'Gateway' },
{ id: 'pi_002', hostname: 'pi-monitor-01', ip: '192.168.1.102', status: 'online', cpu: 12, memory: 28, disk: 22, uptime: '89d 14h', role: 'Monitor' },
{ id: 'pi_003', hostname: 'pi-sensor-01', ip: '192.168.1.103', status: 'online', cpu: 8, memory: 18, disk: 15, uptime: '67d 3h', role: 'Sensor' },
{ id: 'pi_004', hostname: 'pi-relay-01', ip: '192.168.1.104', status: 'warning', cpu: 78, memory: 82, disk: 91, uptime: '34d 22h', role: 'Relay' },
{ id: 'pi_005', hostname: 'pi-backup-01', ip: '192.168.1.105', status: 'online', cpu: 15, memory: 32, disk: 67, uptime: '12d 8h', role: 'Backup' },
{ id: 'pi_006', hostname: 'pi-edge-01', ip: '192.168.1.106', status: 'offline', cpu: 0, memory: 0, disk: 0, uptime: '0d 0h', role: 'Edge Node' }
],
// Runbooks - Operational Procedures
runbooks: [
{ id: 'rb_001', title: 'Emergency Miner Shutdown', category: 'Operations', lastUpdated: '2025-11-10', author: 'OpsTeam', content: '# Emergency Miner Shutdown\n\n## When to Use\n- Temperature exceeds 80°C\n- Power fluctuations detected\n- Network instability\n\n## Steps\n1. Navigate to Miners Dashboard\n2. Select affected miner\n3. Click "Emergency Stop"\n4. Wait for confirmation\n5. Investigate root cause' },
{ id: 'rb_002', title: 'FINRA Compliance Review Workflow', category: 'Compliance', lastUpdated: '2025-11-08', author: 'ComplianceTeam', content: '# FINRA Compliance Review\n\n## Overview\nAll marketing materials require FINRA 2210 review.\n\n## Process\n1. Submit content to Compliance Hub\n2. Automated pre-check runs\n3. Manual review by compliance officer\n4. Revisions if needed\n5. Final approval and archival' },
{ id: 'rb_003', title: 'Pi Device Provisioning', category: 'Infrastructure', lastUpdated: '2025-11-05', author: 'InfraTeam', content: '# Pi Device Setup\n\n## Initial Setup\n1. Flash SD card with BlackRoad OS image\n2. Configure network settings\n3. Install monitoring agent\n4. Register with central dashboard\n5. Run health checks' },
{ id: 'rb_004', title: 'Identity Ledger Backup', category: 'Security', lastUpdated: '2025-11-01', author: 'SecurityTeam', content: '# SHA∞ Ledger Backup\n\n## Frequency\nDaily at 03:00 UTC\n\n## Process\n1. Initiate snapshot\n2. Encrypt with GPG\n3. Upload to secure storage\n4. Verify integrity\n5. Rotate old backups' },
{ id: 'rb_005', title: 'AUM Reconciliation', category: 'Finance', lastUpdated: '2025-10-28', author: 'FinanceTeam', content: '# Assets Under Management Reconciliation\n\n## Monthly Process\n1. Export portfolio data\n2. Cross-reference with custodian reports\n3. Identify discrepancies\n4. Resolve variances\n5. Generate final report' }
],
// Compliance Hub - Audit Logs & Reviews
complianceQueue: [
{ id: 'comp_001', type: 'Marketing Review', content: 'Q4 2025 Investment Newsletter', status: 'pending', submittedBy: 'Marketing', submittedAt: '2025-11-15 14:30', priority: 'high' },
{ id: 'comp_002', type: 'SEC Filing', content: 'Form ADV Amendment', status: 'in_review', submittedBy: 'Legal', submittedAt: '2025-11-14 09:00', priority: 'critical' },
{ id: 'comp_003', type: 'Client Communication', content: 'Fee Schedule Update Email', status: 'approved', submittedBy: 'ClientServices', submittedAt: '2025-11-13 16:45', priority: 'medium' },
{ id: 'comp_004', type: 'Social Media Post', content: 'LinkedIn market commentary', status: 'rejected', submittedBy: 'Marketing', submittedAt: '2025-11-12 11:20', priority: 'low' },
{ id: 'comp_005', type: 'Website Update', content: 'New strategy page content', status: 'pending', submittedBy: 'Marketing', submittedAt: '2025-11-11 10:15', priority: 'medium' }
],
auditLogs: [
{ id: 'audit_001', event: 'User Login', user: 'john.doe@blackroad.io', timestamp: '2025-11-16 09:15:23', ip: '203.0.113.42', result: 'success' },
{ id: 'audit_002', event: 'Portfolio Access', user: 'jane.smith@blackroad.io', timestamp: '2025-11-16 09:12:10', ip: '203.0.113.43', result: 'success' },
{ id: 'audit_003', event: 'Configuration Change', user: 'admin@blackroad.io', timestamp: '2025-11-16 08:45:33', ip: '203.0.113.1', result: 'success' },
{ id: 'audit_004', event: 'Failed Login Attempt', user: 'unknown', timestamp: '2025-11-16 08:22:15', ip: '198.51.100.88', result: 'failure' },
{ id: 'audit_005', event: 'Data Export', user: 'compliance@blackroad.io', timestamp: '2025-11-16 07:30:00', ip: '203.0.113.44', result: 'success' }
],
// Finance & AUM - Portfolio Data
portfolios: [
{ id: 'port_001', name: 'Conservative Growth', aum: 12500000, accounts: 45, ytdReturn: 8.2, benchmark: 'AGG', allocation: { equities: 40, bonds: 50, alternatives: 10 } },
{ id: 'port_002', name: 'Balanced Strategy', aum: 28750000, accounts: 89, ytdReturn: 11.5, benchmark: '60/40', allocation: { equities: 60, bonds: 35, alternatives: 5 } },
{ id: 'port_003', name: 'Aggressive Growth', aum: 15300000, accounts: 32, ytdReturn: 18.7, benchmark: 'S&P 500', allocation: { equities: 85, bonds: 10, alternatives: 5 } },
{ id: 'port_004', name: 'Income Focus', aum: 9200000, accounts: 67, ytdReturn: 5.4, benchmark: 'Barclays Agg', allocation: { equities: 20, bonds: 70, alternatives: 10 } },
{ id: 'port_005', name: 'Alternative Strategies', aum: 22100000, accounts: 15, ytdReturn: 14.3, benchmark: 'HFRI', allocation: { equities: 30, bonds: 20, alternatives: 50 } }
],
annuityProducts: [
{ id: 'ann_001', carrier: 'MetLife', product: 'Guaranteed Income Plus', currentValue: 450000, guaranteedRate: 3.5, riders: ['Death Benefit', 'LTC'] },
{ id: 'ann_002', carrier: 'Prudential', product: 'FlexGuard Annuity', currentValue: 780000, guaranteedRate: 4.2, riders: ['Income Rider'] },
{ id: 'ann_003', carrier: 'Jackson National', product: 'Perspective II', currentValue: 320000, guaranteedRate: 3.8, riders: ['GMWB'] }
],
marketSnapshot: {
sp500: { value: 5928.45, change: 0.82, changePercent: 1.4 },
nasdaq: { value: 18932.12, change: -23.45, changePercent: -0.12 },
dow: { value: 43821.09, change: 156.33, changePercent: 0.36 },
vix: { value: 14.23, change: -0.45, changePercent: -3.07 },
gold: { value: 2638.50, change: 12.30, changePercent: 0.47 },
bitcoin: { value: 91234.78, change: 1823.45, changePercent: 2.04 }
},
// Identity Ledger (SHA∞) - Hashed Identities
identityHashes: Array.from({ length: 200 }, (_, i) => ({
id: `sha_${String(i + 1).padStart(3, '0')}`,
hash: `SHA∞_${Math.random().toString(36).substring(2, 15).toUpperCase()}_${Math.random().toString(36).substring(2, 15).toUpperCase()}`,
timestamp: new Date(Date.now() - Math.random() * 90 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
depth: Math.floor(Math.random() * 10) + 1,
verified: Math.random() > 0.1,
eventType: ['registration', 'verification', 'update', 'access'][Math.floor(Math.random() * 4)]
})),
// Research Lab (Lucidia) - Experiments
experiments: [
{ id: 'exp_001', title: 'Quantum Portfolio Optimization', status: 'active', progress: 67, researcher: 'Dr. Chen', startDate: '2025-09-15', description: 'Applying quantum annealing to portfolio allocation' },
{ id: 'exp_002', title: 'SHA∞ Fractal Depth Analysis', status: 'active', progress: 42, researcher: 'Dr. Patel', startDate: '2025-10-01', description: 'Exploring recursive hash patterns' },
{ id: 'exp_003', title: 'Neural Market Prediction', status: 'completed', progress: 100, researcher: 'AI Team', startDate: '2025-07-20', description: 'Transformer-based market forecasting' },
{ id: 'exp_004', title: 'Decentralized Identity Protocol', status: 'active', progress: 88, researcher: 'Security Team', startDate: '2025-08-10', description: 'Zero-knowledge proof integration' },
{ id: 'exp_005', title: 'Edge Computing for Mining', status: 'planning', progress: 15, researcher: 'Infrastructure', startDate: '2025-11-01', description: 'Pi-based distributed mining nodes' }
],
// System Events
systemEvents: [
{ timestamp: '2025-11-16 09:23:45', level: 'info', source: 'OS', message: 'System boot completed' },
{ timestamp: '2025-11-16 09:22:10', level: 'info', source: 'WindowManager', message: 'Desktop initialized' },
{ timestamp: '2025-11-16 09:21:33', level: 'warning', source: 'MinerMonitor', message: 'Miner Delta offline' },
{ timestamp: '2025-11-16 09:20:55', level: 'info', source: 'PiOps', message: 'All Pi devices synced' },
{ timestamp: '2025-11-16 09:19:12', level: 'error', source: 'API', message: 'Portfolio service timeout' },
{ timestamp: '2025-11-16 09:18:00', level: 'info', source: 'Compliance', message: 'Daily review queue updated' }
],
// Notifications
notifications: [
{ id: 'notif_001', type: 'warning', title: 'Miner Offline', message: 'BlackRoad-Delta has stopped responding', timestamp: '2025-11-16 09:21:33', read: false },
{ id: 'notif_002', type: 'error', title: 'Agent Run Failed', message: 'PortfolioRebalance agent encountered an error', timestamp: '2025-11-16 09:20:30', read: false },
{ id: 'notif_003', type: 'info', title: 'Backup Complete', message: 'Daily backup finished successfully', timestamp: '2025-11-16 03:00:15', read: true },
{ id: 'notif_004', type: 'success', title: 'Compliance Approved', message: 'Fee Schedule Update Email has been approved', timestamp: '2025-11-15 16:52:00', read: true }
],
// Chaos Inbox capture items
captureItems: [
{ id: 1, type: 'note', raw_content: 'Call back Jamie re: brand refresh', source: 'mobile', tags: ['marketing'], status: 'inbox', created_at: '2025-11-12' },
{ id: 2, type: 'link', raw_content: 'https://example.com/roadchain-deck', source: 'web_capture', tags: ['roadchain'], status: 'clustered', created_at: '2025-11-10' },
{ id: 3, type: 'idea', raw_content: 'Course outline: GPU confidence bootcamp', source: 'manual', tags: ['education', 'hardware'], status: 'resurfaced', created_at: '2025-11-01' },
{ id: 4, type: 'screenshot', raw_content: 'Screenshot: confusing AWS invoice UI', source: 'desktop', tags: ['compliance'], status: 'inbox', created_at: '2025-10-28' }
],
captureClusters: [
{ id: 1, name: 'Hardware & PiOps', description: 'Troubleshooting notes and hardware tasks', item_ids: [3, 4] },
{ id: 2, name: 'Marketing & Brand', description: 'Content drafts and approvals', item_ids: [1, 2] }
],
// Unified identity profile
identityProfile: {
name: 'BlackRoad Pilot',
legal_name: 'BlackRoad Pilot',
email: 'pilot@blackroad.io',
phone: '+1-555-123-4567',
address: '1 Infinite Road, Neo City',
timezone: 'UTC',
pronouns: 'they/them',
avatar_url: '',
external_ids: { github: 'pilot', discord: 'pilot#0001' }
},
// Creator workspace
creativeProjects: [
{ id: 1, title: 'RoadStudio Lite Launch Video', type: 'video', status: 'in_production', description: '3 minute walkthrough for creators', links_to_assets: ['https://drive.example.com/video'], revenue_streams: { youtube: 200 }, notes: 'Need new b-roll of OS desktop' },
{ id: 2, title: 'GPU Confidence Course', type: 'course', status: 'drafting', description: 'Micro-course to make GPUs less scary', links_to_assets: ['notion://gpu-course-outline'], revenue_streams: { preorders: 12 }, notes: 'Pair with PiOps demo' }
],
// Corporate Departments
departments: [
{ id: 'dept_hr', name: 'Human Resources', icon: '👥', color: '#5AF' },
{ id: 'dept_legal', name: 'Legal', icon: '⚖️', color: '#A0F' },
{ id: 'dept_finance', name: 'Finance Admin', icon: '💰', color: '#0FA' },
{ id: 'dept_infra', name: 'Infrastructure', icon: '🔧', color: '#FA0' },
{ id: 'dept_agents', name: 'Agent Operations', icon: '🤖', color: '#F55' }
],
// Engineering Diagnostics
diagnostics: {
osVersion: '0.1.0-alpha',
buildDate: '2025-11-16',
uptime: '2h 15m 33s',
activeWindows: 0,
registeredApps: 12,
eventBusMessages: 247,
memoryUsage: '45.2 MB',
theme: 'tealOS'
}
};
// Utility function to get random subset
MockData.getRandomSubset = function(array, count) {
const shuffled = [...array].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
};
// Make globally available
window.MockData = MockData;

827
backend/static/js/os.js Normal file
View File

@@ -0,0 +1,827 @@
/**
* BlackRoad OS - Window Manager & Event Bus
* Core operating system functionality for window management and global events
*
* Features:
* - Window lifecycle management (create, focus, minimize, restore, close)
* - Drag-and-drop window positioning
* - Z-index management with overflow protection
* - Event bus for app communication
* - Notification system
* - Keyboard navigation and shortcuts
*
* Architecture:
* - Uses Map for O(1) window lookups
* - Event-driven design for loose coupling
* - Accessible-first with ARIA attributes
*
* TODO v0.2.0: Add window resizing support
* TODO v0.2.0: Add window maximize functionality
* TODO v0.3.0: Add window snapping/tiling
* TODO v0.3.0: Add window position persistence (localStorage)
*/
class BlackRoadOS {
constructor() {
this.windows = new Map(); // windowId -> { id, element, title, icon, minimized }
this.zIndexCounter = 100;
this.zIndexMax = 9999; // Prevent overflow
this.eventBus = new EventEmitter();
this.windowsContainer = null;
this.taskbarWindows = null;
this.commandPalette = null;
// App lifecycle hooks registry
this.lifecycleHooks = {
onWindowCreated: [],
onWindowFocused: [],
onWindowMinimized: [],
onWindowRestored: [],
onWindowClosed: []
};
this.init();
}
init() {
this.windowsContainer = document.getElementById('windows-container');
this.taskbarWindows = document.getElementById('taskbar-windows');
// Setup global event listeners
this.setupGlobalListeners();
// Emit boot event
this.eventBus.emit('os:boot', { timestamp: new Date().toISOString() });
console.log('✅ BlackRoad OS initialized');
}
/**
* Setup global keyboard shortcuts and event listeners
*/
setupGlobalListeners() {
// Close focused window on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const focusedWindow = this.getFocusedWindow();
if (focusedWindow) {
this.closeWindow(focusedWindow.id);
}
}
});
// Command palette on Ctrl+K (future feature)
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
this.toggleCommandPalette();
}
});
// Click on desktop to unfocus all windows
document.getElementById('desktop')?.addEventListener('click', (e) => {
if (e.target.id === 'desktop' || e.target.classList.contains('desktop-icons')) {
this.unfocusAllWindows();
}
});
}
/**
* Register a lifecycle hook
* @param {string} hookName - onWindowCreated, onWindowFocused, etc.
* @param {Function} callback - Function to call when event occurs
*/
registerLifecycleHook(hookName, callback) {
if (this.lifecycleHooks[hookName]) {
this.lifecycleHooks[hookName].push(callback);
} else {
console.warn(`Unknown lifecycle hook: ${hookName}`);
}
}
/**
* Call all registered lifecycle hooks for an event
* @param {string} hookName - The hook name
* @param {Object} data - Data to pass to callbacks
*/
callLifecycleHooks(hookName, data) {
if (this.lifecycleHooks[hookName]) {
this.lifecycleHooks[hookName].forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Error in lifecycle hook ${hookName}:`, error);
}
});
}
}
/**
* Create a new window or focus existing one
* @param {Object} options - Window configuration
* @param {string} options.id - Unique window identifier (recommended to match app ID)
* @param {string} options.title - Window title
* @param {string} options.icon - Icon emoji or HTML
* @param {HTMLElement|string} options.content - Window content
* @param {string} options.width - CSS width (default: '800px')
* @param {string} options.height - CSS height (default: '600px')
* @param {number} options.x - X position in pixels (optional, will center if not provided)
* @param {number} options.y - Y position in pixels (optional, will center if not provided)
* @param {HTMLElement} options.toolbar - Optional toolbar element
* @param {HTMLElement} options.statusBar - Optional status bar element
* @param {boolean} options.noPadding - Remove padding from content area
* @returns {string} windowId
*/
createWindow(options) {
const windowId = options.id || `window_${Date.now()}`;
// Window deduplication: if window already exists, focus it instead of creating duplicate
if (this.windows.has(windowId)) {
console.log(`🔄 Window "${windowId}" already exists - focusing existing instance`);
const windowData = this.windows.get(windowId);
// If minimized, restore it
if (windowData.minimized) {
this.restoreWindow(windowId);
} else {
this.focusWindow(windowId);
}
return windowId;
}
// Create window element
const windowEl = document.createElement('div');
windowEl.className = 'os-window opening';
windowEl.id = windowId;
windowEl.setAttribute('role', 'dialog');
windowEl.setAttribute('aria-label', options.title || 'Untitled Window');
windowEl.style.width = options.width || '800px';
windowEl.style.height = options.height || '600px';
// Position window: use provided coords or center with cascade offset
if (options.x !== undefined && options.y !== undefined) {
windowEl.style.left = `${options.x}px`;
windowEl.style.top = `${options.y}px`;
} else {
// Center with cascade offset to avoid perfect stacking
const offsetX = (this.windows.size * 30) % 100;
const offsetY = (this.windows.size * 30) % 100;
windowEl.style.left = `calc(50% - ${parseInt(options.width || 800) / 2}px + ${offsetX}px)`;
windowEl.style.top = `calc(50% - ${parseInt(options.height || 600) / 2}px + ${offsetY}px)`;
}
windowEl.style.zIndex = this.getNextZIndex();
// Create titlebar
const titlebar = this.createTitlebar(windowId, options);
windowEl.appendChild(titlebar);
// Toolbar (if provided)
if (options.toolbar) {
windowEl.appendChild(options.toolbar);
}
// Window content
const content = document.createElement('div');
content.className = 'window-content';
if (options.noPadding) {
content.classList.add('no-padding');
}
if (typeof options.content === 'string') {
content.innerHTML = options.content;
} else if (options.content instanceof HTMLElement) {
content.appendChild(options.content);
}
windowEl.appendChild(content);
// Status bar (if provided)
if (options.statusBar) {
windowEl.appendChild(options.statusBar);
}
// Make draggable
this.makeDraggable(windowEl, titlebar);
// Add to container
this.windowsContainer.appendChild(windowEl);
// Store window data
this.windows.set(windowId, {
id: windowId,
element: windowEl,
title: options.title,
icon: options.icon,
minimized: false
});
// Add to taskbar
this.addToTaskbar(windowId);
// Focus window
this.focusWindow(windowId);
// Emit events
this.eventBus.emit('window:created', { windowId, title: options.title });
this.callLifecycleHooks('onWindowCreated', { windowId, title: options.title });
// Remove opening animation class after animation completes
setTimeout(() => {
windowEl.classList.remove('opening');
}, 200);
console.log(`✨ Created window: "${options.title}" (${windowId})`);
return windowId;
}
/**
* Create window titlebar with controls
* @param {string} windowId - Window identifier
* @param {Object} options - Window options
* @returns {HTMLElement} Titlebar element
*/
createTitlebar(windowId, options) {
const titlebar = document.createElement('div');
titlebar.className = 'window-titlebar';
const titlebarLeft = document.createElement('div');
titlebarLeft.className = 'window-titlebar-left';
if (options.icon) {
const icon = document.createElement('div');
icon.className = 'window-icon';
icon.setAttribute('aria-hidden', 'true');
icon.innerHTML = options.icon;
titlebarLeft.appendChild(icon);
}
const title = document.createElement('div');
title.className = 'window-title';
title.textContent = options.title || 'Untitled Window';
titlebarLeft.appendChild(title);
titlebar.appendChild(titlebarLeft);
// Window controls
const controls = document.createElement('div');
controls.className = 'window-controls';
controls.setAttribute('role', 'group');
controls.setAttribute('aria-label', 'Window controls');
// Minimize button
const minimizeBtn = document.createElement('button');
minimizeBtn.className = 'window-control-btn minimize';
minimizeBtn.innerHTML = '';
minimizeBtn.setAttribute('aria-label', 'Minimize window');
minimizeBtn.title = 'Minimize';
minimizeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.minimizeWindow(windowId);
});
// Maximize button (stub for v0.2.0)
const maximizeBtn = document.createElement('button');
maximizeBtn.className = 'window-control-btn maximize';
maximizeBtn.innerHTML = '□';
maximizeBtn.setAttribute('aria-label', 'Maximize window (coming soon)');
maximizeBtn.title = 'Maximize (coming in v0.2.0)';
maximizeBtn.disabled = true; // Disabled until implemented
maximizeBtn.style.opacity = '0.5';
// TODO v0.2.0: Implement maximize functionality
// Should toggle between normal and fullscreen (minus taskbar)
// Store original size/position for restore
// Add 'maximized' class and update button to restore icon
// Close button
const closeBtn = document.createElement('button');
closeBtn.className = 'window-control-btn close';
closeBtn.innerHTML = '×';
closeBtn.setAttribute('aria-label', 'Close window');
closeBtn.title = 'Close';
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.closeWindow(windowId);
});
controls.appendChild(minimizeBtn);
controls.appendChild(maximizeBtn);
controls.appendChild(closeBtn);
titlebar.appendChild(controls);
return titlebar;
}
/**
* Make window draggable via titlebar
* @param {HTMLElement} windowEl - Window element
* @param {HTMLElement} handle - Drag handle (titlebar)
*/
makeDraggable(windowEl, handle) {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
handle.addEventListener('mousedown', (e) => {
// Don't drag if clicking on buttons or other interactive elements
if (e.target.classList.contains('window-control-btn') || e.target.tagName === 'BUTTON') {
return;
}
isDragging = true;
initialX = e.clientX - windowEl.offsetLeft;
initialY = e.clientY - windowEl.offsetTop;
// Focus window when drag starts
this.focusWindow(windowEl.id);
// Change cursor
handle.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
// Prevent dragging completely off-screen
// Keep at least 100px of window visible
const minVisible = 100;
currentX = Math.max(-windowEl.offsetWidth + minVisible, Math.min(currentX, window.innerWidth - minVisible));
currentY = Math.max(0, Math.min(currentY, window.innerHeight - 100));
windowEl.style.left = `${currentX}px`;
windowEl.style.top = `${currentY}px`;
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
handle.style.cursor = 'move';
}
});
}
/**
* Get next z-index with overflow protection
* @returns {number} Next z-index value
*/
getNextZIndex() {
if (this.zIndexCounter >= this.zIndexMax) {
// Reset z-index when we hit max, re-layer all windows
console.log('🔄 Z-index overflow protection: resetting window layers');
this.reindexWindows();
}
return this.zIndexCounter++;
}
/**
* Reindex all windows to prevent z-index overflow
* Maintains relative stacking order
*/
reindexWindows() {
const sortedWindows = Array.from(this.windows.values())
.sort((a, b) => parseInt(a.element.style.zIndex) - parseInt(b.element.style.zIndex));
this.zIndexCounter = 100;
sortedWindows.forEach(windowData => {
windowData.element.style.zIndex = this.zIndexCounter++;
});
}
/**
* Focus a window (bring to front)
* @param {string} windowId - Window identifier
*/
focusWindow(windowId) {
const windowData = this.windows.get(windowId);
if (!windowData) {
console.warn(`Cannot focus - window not found: ${windowId}`);
return;
}
// Update z-index to bring to front
windowData.element.style.zIndex = this.getNextZIndex();
// Update visual states (only one window should be focused)
this.windows.forEach((w, id) => {
w.element.classList.toggle('focused', id === windowId);
});
// Update taskbar button states
this.updateTaskbar();
// Emit events
this.eventBus.emit('window:focused', { windowId });
this.callLifecycleHooks('onWindowFocused', { windowId });
}
/**
* Unfocus all windows
*/
unfocusAllWindows() {
this.windows.forEach(windowData => {
windowData.element.classList.remove('focused');
});
this.updateTaskbar();
}
/**
* Minimize a window
* @param {string} windowId - Window identifier
*/
minimizeWindow(windowId) {
const windowData = this.windows.get(windowId);
if (!windowData) return;
windowData.minimized = true;
windowData.element.classList.add('minimized');
this.updateTaskbar();
// Emit events
this.eventBus.emit('window:minimized', { windowId });
this.callLifecycleHooks('onWindowMinimized', { windowId });
}
/**
* Restore a minimized window
* @param {string} windowId - Window identifier
*/
restoreWindow(windowId) {
const windowData = this.windows.get(windowId);
if (!windowData) return;
windowData.minimized = false;
windowData.element.classList.remove('minimized');
// Focus when restoring
this.focusWindow(windowId);
// Emit events
this.eventBus.emit('window:restored', { windowId });
this.callLifecycleHooks('onWindowRestored', { windowId });
}
/**
* Close a window
* @param {string} windowId - Window identifier
*/
closeWindow(windowId) {
const windowData = this.windows.get(windowId);
if (!windowData) return;
const windowTitle = windowData.title;
// Emit events before removal (so apps can clean up)
this.eventBus.emit('window:closed', { windowId, title: windowTitle });
this.callLifecycleHooks('onWindowClosed', { windowId, title: windowTitle });
// Remove from DOM
windowData.element.remove();
// Remove from windows map
this.windows.delete(windowId);
// Update taskbar (will remove button)
this.updateTaskbar();
console.log(`❌ Closed window: "${windowTitle}" (${windowId})`);
}
/**
* Add window to taskbar
* @param {string} windowId - Window identifier
*/
addToTaskbar(windowId) {
const windowData = this.windows.get(windowId);
if (!windowData) return;
const btn = document.createElement('button');
btn.className = 'taskbar-window-button';
btn.id = `taskbar-${windowId}`;
btn.textContent = windowData.title;
btn.setAttribute('aria-label', `${windowData.title} window`);
btn.setAttribute('role', 'button');
btn.addEventListener('click', () => {
if (windowData.minimized) {
this.restoreWindow(windowId);
} else {
// If already focused, minimize; otherwise focus
if (windowData.element.classList.contains('focused')) {
this.minimizeWindow(windowId);
} else {
this.focusWindow(windowId);
}
}
});
// Keyboard navigation for taskbar buttons
btn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
btn.click();
}
});
this.taskbarWindows.appendChild(btn);
}
/**
* Update taskbar button states to match window states
*/
updateTaskbar() {
this.windows.forEach((windowData, windowId) => {
const btn = document.getElementById(`taskbar-${windowId}`);
if (btn) {
// Update active state
btn.classList.toggle('active', windowData.element.classList.contains('focused'));
// Update visual opacity for minimized windows
btn.style.opacity = windowData.minimized ? '0.6' : '1';
// Update ARIA state
btn.setAttribute('aria-pressed', windowData.element.classList.contains('focused') ? 'true' : 'false');
}
});
// Remove buttons for closed windows
this.taskbarWindows.querySelectorAll('.taskbar-window-button').forEach(btn => {
const windowId = btn.id.replace('taskbar-', '');
if (!this.windows.has(windowId)) {
btn.remove();
}
});
}
/**
* Get currently focused window
* @returns {Object|null} Window data or null if no window is focused
*/
getFocusedWindow() {
for (let [id, data] of this.windows) {
if (data.element.classList.contains('focused')) {
return data;
}
}
return null;
}
/**
* Get window by ID
* @param {string} windowId - Window identifier
* @returns {Object|null} Window data or null
*/
getWindow(windowId) {
return this.windows.get(windowId) || null;
}
/**
* Get all open windows
* @returns {Array} Array of window data objects
*/
getAllWindows() {
return Array.from(this.windows.values());
}
/**
* Toggle global command palette for unified search
*/
toggleCommandPalette() {
if (!this.commandPalette) {
this.buildCommandPalette();
}
const isVisible = this.commandPalette.classList.contains('open');
if (isVisible) {
this.commandPalette.classList.remove('open');
} else {
this.commandPalette.classList.add('open');
const input = this.commandPalette.querySelector('input');
input.value = '';
input.focus();
this.populatePaletteResults('');
}
}
buildCommandPalette() {
this.commandPalette = document.createElement('div');
this.commandPalette.className = 'command-palette';
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Search apps, notes, and knowledge (Ctrl/Cmd + K)';
input.setAttribute('aria-label', 'Global search');
this.commandPalette.appendChild(input);
const results = document.createElement('div');
results.className = 'command-results';
this.commandPalette.appendChild(results);
input.addEventListener('input', (e) => this.populatePaletteResults(e.target.value));
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.toggleCommandPalette();
});
document.body.appendChild(this.commandPalette);
this.populatePaletteResults('');
}
populatePaletteResults(query) {
if (!this.commandPalette) return;
const resultsContainer = this.commandPalette.querySelector('.command-results');
resultsContainer.innerHTML = '';
const lower = query.toLowerCase();
const appMatches = Object.values(window.AppRegistry).filter(app =>
app.name.toLowerCase().includes(lower) || app.description.toLowerCase().includes(lower)
);
const captureMatches = (window.MockData?.captureItems || []).filter(item =>
!query || (item.raw_content || '').toLowerCase().includes(lower)
).slice(0, 5);
const projectMatches = (window.MockData?.creativeProjects || []).filter(project =>
!query || project.title.toLowerCase().includes(lower)
).slice(0, 5);
const renderGroup = (title, items, onClick) => {
if (!items.length) return;
const group = document.createElement('div');
group.className = 'command-group';
const heading = document.createElement('div');
heading.className = 'command-group-title';
heading.textContent = title;
group.appendChild(heading);
items.forEach(item => {
const row = document.createElement('div');
row.className = 'command-row';
row.textContent = item.label;
row.tabIndex = 0;
row.addEventListener('click', () => onClick(item));
row.addEventListener('keydown', (e) => {
if (e.key === 'Enter') onClick(item);
});
group.appendChild(row);
});
resultsContainer.appendChild(group);
};
renderGroup('Apps', appMatches.map(app => ({ label: `${app.icon} ${app.name}`, id: app.id })), (item) => {
window.launchApp(item.id);
this.toggleCommandPalette();
});
renderGroup('Chaos Inbox', captureMatches.map(c => ({ label: `🌀 ${c.raw_content || c.type}`, id: 'chaos-inbox' })), (item) => {
window.launchApp(item.id);
this.toggleCommandPalette();
});
renderGroup('Creator projects', projectMatches.map(p => ({ label: `🎨 ${p.title}`, id: 'creator-studio' })), (item) => {
window.launchApp(item.id);
this.toggleCommandPalette();
});
if (!resultsContainer.childElementCount) {
resultsContainer.textContent = 'No matches yet. Try searching for an app or project.';
}
}
/**
* Show a toast notification
* @param {Object} options - Notification options
* @param {string} options.type - Notification type (success, error, warning, info)
* @param {string} options.title - Notification title
* @param {string} options.message - Notification message
* @param {number} options.duration - Duration in ms (0 = persistent, default: 5000)
*/
showNotification(options) {
const container = document.getElementById('notification-container');
const notification = document.createElement('div');
notification.className = `notification ${options.type || 'info'}`;
notification.setAttribute('role', 'alert');
notification.setAttribute('aria-live', 'polite');
const header = document.createElement('div');
header.className = 'notification-header';
const title = document.createElement('div');
title.className = 'notification-title';
title.textContent = options.title || 'Notification';
const closeBtn = document.createElement('button');
closeBtn.className = 'notification-close';
closeBtn.innerHTML = '×';
closeBtn.setAttribute('aria-label', 'Close notification');
closeBtn.addEventListener('click', () => {
notification.remove();
});
header.appendChild(title);
header.appendChild(closeBtn);
const body = document.createElement('div');
body.className = 'notification-body';
body.textContent = options.message || '';
notification.appendChild(header);
notification.appendChild(body);
container.appendChild(notification);
// Auto-remove after duration
const duration = options.duration !== undefined ? options.duration : 5000;
if (duration > 0) {
setTimeout(() => {
notification.remove();
}, duration);
}
this.eventBus.emit('notification:shown', options);
}
/**
* Get system diagnostics
* @returns {Object} System diagnostics data
*/
getDiagnostics() {
return {
windowCount: this.windows.size,
focusedWindowId: this.getFocusedWindow()?.id || null,
zIndexCounter: this.zIndexCounter,
eventBusListeners: Object.keys(this.eventBus.events).reduce((acc, key) => {
acc[key] = this.eventBus.events[key].length;
return acc;
}, {})
};
}
}
/**
* Simple Event Emitter for pub/sub communication
* Enables loose coupling between OS and apps
*/
class EventEmitter {
constructor() {
this.events = {};
}
/**
* Register an event listener
* @param {string} event - Event name
* @param {Function} callback - Callback function
*/
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
/**
* Emit an event to all listeners
* @param {string} event - Event name
* @param {*} data - Data to pass to listeners
*/
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Error in event listener for "${event}":`, error);
}
});
}
/**
* Remove an event listener
* @param {string} event - Event name
* @param {Function} callback - Callback to remove
*/
off(event, callback) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
/**
* Remove all listeners for an event
* @param {string} event - Event name
*/
removeAllListeners(event) {
if (event) {
delete this.events[event];
} else {
this.events = {};
}
}
}
// Create global OS instance
window.OS = new BlackRoadOS();

View File

@@ -0,0 +1,200 @@
/**
* Application Registry
* Central registry of all installed apps
* Maps app IDs to metadata and entry points
* TODO: Add app categories/folders
* TODO: Add app permissions system
* TODO: Add dynamic app loading
*/
const AppRegistry = {
// Core Apps
prism: {
id: 'prism',
name: 'Prism Console',
icon: '💠',
description: 'Agent monitoring and system events',
category: 'Core',
entry: window.PrismApp,
defaultSize: { width: '900px', height: '700px' }
},
miners: {
id: 'miners',
name: 'Miners Dashboard',
icon: '⛏️',
description: 'Mining operations and telemetry',
category: 'Operations',
entry: window.MinersApp,
defaultSize: { width: '1000px', height: '700px' }
},
piops: {
id: 'piops',
name: 'Pi Ops',
icon: '🥧',
description: 'Raspberry Pi device management',
category: 'Infrastructure',
entry: window.PiOpsApp,
defaultSize: { width: '900px', height: '650px' }
},
runbooks: {
id: 'runbooks',
name: 'Runbooks',
icon: '📚',
description: 'Operational procedures and guides',
category: 'Documentation',
entry: window.RunbooksApp,
defaultSize: { width: '1100px', height: '750px' }
},
compliance: {
id: 'compliance',
name: 'Compliance Hub',
icon: '✓',
description: 'FINRA reviews and audit logs',
category: 'Compliance',
entry: window.ComplianceApp,
defaultSize: { width: '1000px', height: '700px' }
},
finance: {
id: 'finance',
name: 'Finance & AUM',
icon: '💰',
description: 'Portfolio management and analytics',
category: 'Finance',
entry: window.FinanceApp,
defaultSize: { width: '1100px', height: '750px' }
},
identity: {
id: 'identity',
name: 'Identity Ledger',
icon: '🔐',
description: 'SHA∞ identity system',
category: 'Security',
entry: window.IdentityApp,
defaultSize: { width: '1000px', height: '700px' }
},
research: {
id: 'research',
name: 'Research Lab',
icon: '🔬',
description: 'Lucidia experiments and analysis',
category: 'Research',
entry: window.ResearchApp,
defaultSize: { width: '1000px', height: '700px' }
},
engineering: {
id: 'engineering',
name: 'Engineering',
icon: '🔧',
description: 'DevTools and system diagnostics',
category: 'Development',
entry: window.EngineeringApp,
defaultSize: { width: '900px', height: '700px' }
},
settings: {
id: 'settings',
name: 'Settings',
icon: '⚙️',
description: 'System preferences and configuration',
category: 'System',
entry: window.SettingsApp,
defaultSize: { width: '700px', height: '600px' }
},
notifications: {
id: 'notifications',
name: 'Notifications',
icon: '🔔',
description: 'System alerts and messages',
category: 'System',
entry: window.NotificationsApp,
defaultSize: { width: '500px', height: '600px' }
},
corporate: {
id: 'corporate',
name: 'Corporate OS',
icon: '🏢',
description: 'Department management panels',
category: 'Corporate',
entry: window.CorporateApp,
defaultSize: { width: '800px', height: '600px' }
},
'chaos-inbox': {
id: 'chaos-inbox',
name: 'Chaos Inbox',
icon: '🌀',
description: 'All your scraps in one forgiving place',
category: 'Focus',
entry: window.ChaosInboxApp,
defaultSize: { width: '1100px', height: '720px' }
},
'identity-center': {
id: 'identity-center',
name: 'Identity Center',
icon: '🪪',
description: 'Your info once, used everywhere',
category: 'System',
entry: window.IdentityCenterApp,
defaultSize: { width: '800px', height: '650px' }
},
'creator-studio': {
id: 'creator-studio',
name: 'Creator Studio',
icon: '🎨',
description: 'Home base for creative work',
category: 'Creators',
entry: window.CreatorStudioApp,
defaultSize: { width: '1000px', height: '700px' }
},
'compliance-ops': {
id: 'compliance-ops',
name: 'Compliance & Ops',
icon: '🧭',
description: 'Transparent logs & workflows',
category: 'Compliance',
entry: window.ComplianceOpsApp,
defaultSize: { width: '900px', height: '650px' }
}
};
/**
* Launch an app by ID
*/
function launchApp(appId) {
const app = AppRegistry[appId];
if (!app) {
console.error(`App not found: ${appId}`);
return;
}
if (!app.entry) {
console.error(`App entry point not defined: ${appId}`);
window.OS.showNotification({
type: 'error',
title: 'App Error',
message: `${app.name} is not yet implemented`,
duration: 3000
});
return;
}
// Call the app's entry function
app.entry();
}
// Make globally available
window.AppRegistry = AppRegistry;
window.launchApp = launchApp;

247
backend/static/js/theme.js Normal file
View File

@@ -0,0 +1,247 @@
/**
* BlackRoad OS Theme Manager
* Handles theme switching and persistence
*
* Built-in Themes:
* - TealOS (default): Teal/cyan cyberpunk aesthetic
* - NightOS: Purple/magenta dark theme
*
* Features:
* - Theme persistence via localStorage
* - Smooth transitions between themes
* - Event emission for theme changes
* - Extensible architecture for custom themes
*
* Theme System Architecture:
* - Themes are defined via CSS variables in styles.css
* - Body attribute `data-theme` controls which CSS vars are active
* - All components reference CSS variables, not hardcoded colors
* - New themes can be added by adding `body[data-theme="name"]` blocks
*
* TODO v0.2.0: Add theme preview system
* TODO v0.2.0: Add custom theme builder in Settings app
* TODO v0.3.0: Support theme import/export (JSON format)
* TODO v0.3.0: Add system theme (auto dark/light based on OS preference)
*/
class ThemeManager {
constructor() {
this.currentTheme = 'tealOS';
this.availableThemes = ['tealOS', 'nightOS', 'contrastOS']; // Extensible list
// TODO v0.2.0: Load available themes dynamically from CSS
this.init();
}
init() {
// Load saved theme preference from localStorage
const saved = localStorage.getItem('blackroad-theme');
if (saved && this.availableThemes.includes(saved)) {
this.currentTheme = saved;
}
// Apply theme immediately (before page renders)
this.applyTheme(this.currentTheme);
// Setup toggle button
this.setupToggleButton();
console.log(`🎨 Theme Manager initialized: ${this.currentTheme}`);
}
/**
* Setup theme toggle button in system tray
*/
setupToggleButton() {
const toggleBtn = document.getElementById('theme-toggle');
if (!toggleBtn) return;
// Add accessibility attributes
toggleBtn.setAttribute('aria-label', 'Toggle theme');
toggleBtn.setAttribute('aria-pressed', 'false');
toggleBtn.addEventListener('click', () => {
this.toggleTheme();
});
// Keyboard support
toggleBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.toggleTheme();
}
});
this.updateToggleButton();
}
/**
* Toggle between available themes
* Currently cycles between TealOS and NightOS
* TODO v0.2.0: Support more than 2 themes with dropdown or cycle logic
*/
toggleTheme() {
// Cycle through available themes
const currentIndex = this.availableThemes.indexOf(this.currentTheme);
const nextIndex = (currentIndex + 1) % this.availableThemes.length;
this.currentTheme = this.availableThemes[nextIndex];
this.applyTheme(this.currentTheme);
this.saveTheme();
this.updateToggleButton();
// Emit event so apps can react if needed
if (window.OS) {
window.OS.eventBus.emit('theme:changed', {
theme: this.currentTheme,
previousTheme: this.currentTheme === 'tealOS' ? 'nightOS' : 'tealOS'
});
// Show notification
const themeName = this.currentTheme === 'tealOS' ? 'Teal OS' : 'Night OS';
window.OS.showNotification({
type: 'info',
title: 'Theme Changed',
message: `Switched to ${themeName}`,
duration: 2000
});
}
console.log(`🎨 Theme switched to: ${this.currentTheme}`);
}
/**
* Apply a theme by setting data-theme attribute
* @param {string} theme - Theme identifier (e.g., 'tealOS', 'nightOS')
*/
applyTheme(theme) {
if (!this.availableThemes.includes(theme)) {
console.warn(`Unknown theme: ${theme}. Falling back to tealOS`);
theme = 'tealOS';
}
// Apply theme with smooth transition
document.body.classList.add('theme-transitioning');
document.body.setAttribute('data-theme', theme);
// Remove transition class after animation completes
setTimeout(() => {
document.body.classList.remove('theme-transitioning');
}, 300);
}
/**
* Save current theme to localStorage
*/
saveTheme() {
localStorage.setItem('blackroad-theme', this.currentTheme);
}
/**
* Update toggle button icon to reflect current theme
*/
updateToggleButton() {
const toggleBtn = document.getElementById('theme-toggle');
if (!toggleBtn) return;
const icon = toggleBtn.querySelector('.icon');
if (icon) {
const iconMap = { tealOS: '🌙', nightOS: '☀️', contrastOS: '⚡️' };
icon.textContent = iconMap[this.currentTheme] || '🎨';
}
// Update aria-label for clarity
const currentIndex = this.availableThemes.indexOf(this.currentTheme);
const nextTheme = this.availableThemes[(currentIndex + 1) % this.availableThemes.length] || 'Teal OS';
toggleBtn.setAttribute('aria-label', `Switch to ${nextTheme}`);
}
/**
* Get current theme
* @returns {string} Current theme identifier
*/
getTheme() {
return this.currentTheme;
}
/**
* Set theme programmatically
* @param {string} theme - Theme identifier
*/
setTheme(theme) {
if (this.availableThemes.includes(theme)) {
this.currentTheme = theme;
this.applyTheme(theme);
this.saveTheme();
this.updateToggleButton();
// Emit event
if (window.OS) {
window.OS.eventBus.emit('theme:changed', { theme });
}
} else {
console.error(`Cannot set theme: ${theme} is not available`);
}
}
/**
* Get list of available themes
* @returns {Array} Array of theme identifiers
*/
getAvailableThemes() {
return [...this.availableThemes];
}
/**
* Get theme metadata (for Settings app or theme picker)
* @param {string} theme - Theme identifier
* @returns {Object} Theme metadata
*/
getThemeMetadata(theme) {
const metadata = {
tealOS: {
id: 'tealOS',
name: 'Teal OS',
description: 'Cyberpunk teal/cyan aesthetic with dark background',
primaryColor: '#0FA',
author: 'BlackRoad Team',
preview: null // TODO v0.2.0: Add preview image
},
nightOS: {
id: 'nightOS',
name: 'Night OS',
description: 'Purple/magenta dark theme',
primaryColor: '#A0F',
author: 'BlackRoad Team',
preview: null
}
};
return metadata[theme] || null;
}
/**
* Preview a theme without committing (for theme picker)
* TODO v0.2.0: Implement preview mode with cancel/apply buttons
* @param {string} theme - Theme to preview
*/
previewTheme(theme) {
console.log(`🔍 Preview mode for theme: ${theme} - Coming in v0.2.0`);
// Would temporarily apply theme without saving
// Settings app would show "Apply" and "Cancel" buttons
}
/**
* Register a custom theme (extension point for future custom theme system)
* TODO v0.3.0: Allow apps to register custom themes dynamically
* @param {Object} themeDefinition - Theme definition object
*/
registerCustomTheme(themeDefinition) {
console.log('📦 Custom theme registration - Coming in v0.3.0');
// Would validate theme definition
// Add CSS variables dynamically
// Add to availableThemes list
}
}
// Create global instance
window.ThemeManager = new ThemeManager();