mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 07:57:19 -05:00
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:
391
backend/static/js/app.js
Normal file
391
backend/static/js/app.js
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user