feat: introduce routing system

This commit is contained in:
2025-10-23 11:38:03 +02:00
parent c6949c36c1
commit 531ddbee85
4 changed files with 69 additions and 16 deletions

View File

@@ -19,12 +19,7 @@ const PORT = process.env.PORT || 3000;
// Serve static files from public directory // Serve static files from public directory
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
// Serve the main HTML page // Health check endpoint (before catch-all route)
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Health check endpoint
app.get('/health', (req, res) => { app.get('/health', (req, res) => {
res.json({ res.json({
status: 'healthy', status: 'healthy',
@@ -34,6 +29,13 @@ app.get('/health', (req, res) => {
}); });
}); });
// SPA catch-all route - serves index.html for all routes
// This allows client-side routing to work properly
// Using regex pattern for Express 5 compatibility
app.get(/^\/(?!health$).*/, (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Start the server // Start the server
app.listen(PORT, '0.0.0.0', () => { app.listen(PORT, '0.0.0.0', () => {
console.log(`SPORE UI Frontend Server is running on http://0.0.0.0:${PORT}`); console.log(`SPORE UI Frontend Server is running on http://0.0.0.0:${PORT}`);

View File

@@ -18,7 +18,7 @@
</svg> </svg>
</button> </button>
<div class="nav-left"> <div class="nav-left">
<button class="nav-tab active" data-view="cluster"> <a href="/cluster" class="nav-tab active" data-view="cluster">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
<circle cx="12" cy="12" r="9"/> <circle cx="12" cy="12" r="9"/>
<circle cx="8" cy="10" r="1.5"/> <circle cx="8" cy="10" r="1.5"/>
@@ -27,8 +27,8 @@
<path d="M9 11l3 3M9 11l6-3"/> <path d="M9 11l3 3M9 11l6-3"/>
</svg> </svg>
Cluster Cluster
</button> </a>
<button class="nav-tab" data-view="topology"> <a href="/topology" class="nav-tab" data-view="topology">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
<circle cx="12" cy="4" r="1.6"/> <circle cx="12" cy="4" r="1.6"/>
<circle cx="19" cy="9" r="1.6"/> <circle cx="19" cy="9" r="1.6"/>
@@ -38,20 +38,20 @@
<path d="M12 4L16 18M16 18L5 9M5 9L19 9M19 9L8 18M8 18L12 4"/> <path d="M12 4L16 18M16 18L5 9M5 9L19 9M19 9L8 18M8 18L12 4"/>
</svg> </svg>
Topology Topology
</button> </a>
<button class="nav-tab" data-view="monitoring"> <a href="/monitoring" class="nav-tab" data-view="monitoring">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
<path d="M3 12h3l2 7 4-14 3 10 2-6h4"/> <path d="M3 12h3l2 7 4-14 3 10 2-6h4"/>
</svg> </svg>
Monitoring Monitoring
</button> </a>
<button class="nav-tab" data-view="firmware"> <a href="/firmware" class="nav-tab" data-view="firmware">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
<path d="M4 7l8-4 8 4v10l-8 4-8-4z"/> <path d="M4 7l8-4 8 4v10l-8 4-8-4z"/>
<path d="M12 8v8"/> <path d="M12 8v8"/>
</svg> </svg>
Firmware Firmware
</button> </a>
</div> </div>
<div class="nav-right"> <div class="nav-right">
<div class="theme-switcher"> <div class="theme-switcher">

View File

@@ -647,7 +647,7 @@ class App {
} }
// Navigate to a route // Navigate to a route
navigateTo(routeName) { navigateTo(routeName, updateUrl = true) {
// Check cooldown period // Check cooldown period
const now = Date.now(); const now = Date.now();
if (now - this.lastNavigationTime < this.navigationCooldown) { if (now - this.lastNavigationTime < this.navigationCooldown) {
@@ -670,10 +670,45 @@ class App {
return; return;
} }
// Update URL if requested
if (updateUrl) {
this.updateURL(routeName);
}
this.lastNavigationTime = now; this.lastNavigationTime = now;
this.performNavigation(routeName); this.performNavigation(routeName);
} }
// Update browser URL
updateURL(routeName) {
const url = `/${routeName}`;
if (window.location.pathname !== url) {
window.history.pushState({ route: routeName }, '', url);
logger.debug(`App: Updated URL to ${url}`);
}
}
// Get route from current URL
getRouteFromURL() {
const path = window.location.pathname;
// Remove leading slash and use as route name
const routeName = path.substring(1) || 'cluster'; // default to cluster
return routeName;
}
// Handle browser back/forward
handlePopState(event) {
if (event.state && event.state.route) {
logger.debug(`App: Handling popstate for route '${event.state.route}'`);
this.navigateTo(event.state.route, false); // Don't update URL again
} else {
// Fallback: parse URL
const routeName = this.getRouteFromURL();
logger.debug(`App: Handling popstate, navigating to '${routeName}'`);
this.navigateTo(routeName, false);
}
}
// Perform the actual navigation // Perform the actual navigation
async performNavigation(routeName) { async performNavigation(routeName) {
this.navigationInProgress = true; this.navigationInProgress = true;
@@ -848,12 +883,27 @@ class App {
// Setup navigation // Setup navigation
setupNavigation() { setupNavigation() {
// Intercept navigation link clicks
document.querySelectorAll((window.CONSTANTS && window.CONSTANTS.SELECTORS.NAV_TAB) || '.nav-tab').forEach(tab => { document.querySelectorAll((window.CONSTANTS && window.CONSTANTS.SELECTORS.NAV_TAB) || '.nav-tab').forEach(tab => {
tab.addEventListener('click', () => { tab.addEventListener('click', (e) => {
e.preventDefault(); // Prevent default link behavior
const routeName = tab.dataset.view; const routeName = tab.dataset.view;
this.navigateTo(routeName); this.navigateTo(routeName);
}); });
}); });
// Handle browser back/forward buttons
window.addEventListener('popstate', (e) => this.handlePopState(e));
// Navigate to route based on current URL on initial load
const initialRoute = this.getRouteFromURL();
logger.debug(`App: Initial route from URL: '${initialRoute}'`);
// Set initial history state
window.history.replaceState({ route: initialRoute }, '', `/${initialRoute}`);
// Navigate to initial route
this.navigateTo(initialRoute, false); // Don't update URL since we just set it
} }
// Clean up cached components (call when app is shutting down) // Clean up cached components (call when app is shutting down)

View File

@@ -128,6 +128,7 @@ button:disabled {
justify-content: center; justify-content: center;
gap: 0.5rem; gap: 0.5rem;
transition: all 0.2s ease; transition: all 0.2s ease;
text-decoration: none;
} }
.nav-tab:hover { .nav-tab:hover {