diff --git a/index.js b/index.js
index 21b13de..5a03a94 100644
--- a/index.js
+++ b/index.js
@@ -19,12 +19,7 @@ const PORT = process.env.PORT || 3000;
// Serve static files from public directory
app.use(express.static(path.join(__dirname, 'public')));
-// Serve the main HTML page
-app.get('/', (req, res) => {
- res.sendFile(path.join(__dirname, 'public', 'index.html'));
-});
-
-// Health check endpoint
+// Health check endpoint (before catch-all route)
app.get('/health', (req, res) => {
res.json({
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
app.listen(PORT, '0.0.0.0', () => {
console.log(`SPORE UI Frontend Server is running on http://0.0.0.0:${PORT}`);
diff --git a/public/index.html b/public/index.html
index b54c48b..9bc94de 100644
--- a/public/index.html
+++ b/public/index.html
@@ -18,7 +18,7 @@
diff --git a/public/scripts/framework.js b/public/scripts/framework.js
index 68d0837..bb49a88 100644
--- a/public/scripts/framework.js
+++ b/public/scripts/framework.js
@@ -647,7 +647,7 @@ class App {
}
// Navigate to a route
- navigateTo(routeName) {
+ navigateTo(routeName, updateUrl = true) {
// Check cooldown period
const now = Date.now();
if (now - this.lastNavigationTime < this.navigationCooldown) {
@@ -670,10 +670,45 @@ class App {
return;
}
+ // Update URL if requested
+ if (updateUrl) {
+ this.updateURL(routeName);
+ }
+
this.lastNavigationTime = now;
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
async performNavigation(routeName) {
this.navigationInProgress = true;
@@ -848,12 +883,27 @@ class App {
// Setup navigation
setupNavigation() {
+ // Intercept navigation link clicks
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;
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)
diff --git a/public/styles/main.css b/public/styles/main.css
index 0ee9fda..d7ad3eb 100644
--- a/public/styles/main.css
+++ b/public/styles/main.css
@@ -128,6 +128,7 @@ button:disabled {
justify-content: center;
gap: 0.5rem;
transition: all 0.2s ease;
+ text-decoration: none;
}
.nav-tab:hover {