@@ -14,7 +14,7 @@ use crate::security::key_store::{KeyStore, KeyStoreBackend, KeyStoreConfig};
1414use crate :: security:: { middleware:: SecurityMiddleware , SecurityConfig } ;
1515use crate :: server:: LoxoneMcpServer ;
1616pub use authentication:: AuthConfig ;
17- use authentication:: { auth_middleware , AuthManager } ;
17+ use authentication:: AuthManager ;
1818use mcp_foundation:: { Content , ServerHandler } ;
1919use rate_limiting:: { EnhancedRateLimiter , RateLimitResult } ;
2020
@@ -618,7 +618,20 @@ impl HttpTransportServer {
618618 crate :: monitoring:: key_management_ui:: create_key_management_router (
619619 shared_state. key_store . clone ( ) ,
620620 ) ;
621- let app = app. nest ( "/admin" , key_management_router) ;
621+
622+ // Add main navigation hub
623+ let nav_router = Router :: new ( )
624+ . route ( "/" , get ( navigation_hub) )
625+ . merge ( key_management_router) ;
626+
627+ // Add admin routes
628+ let app = app
629+ . nest ( "/admin" , nav_router)
630+ . route ( "/admin" , get ( admin_redirect) ) ; // Redirect /admin to /admin/
631+ info ! (
632+ "🏠 Navigation Hub: http://localhost:{}/admin (with API key)" ,
633+ self . port
634+ ) ;
622635 info ! (
623636 "🔑 API key management UI: http://localhost:{}/admin/keys" ,
624637 self . port
@@ -644,6 +657,359 @@ struct AppState {
644657 key_store : Arc < KeyStore > ,
645658}
646659
660+ /// Redirect from /admin to /admin/
661+ async fn admin_redirect ( ) -> impl IntoResponse {
662+ axum:: response:: Redirect :: permanent ( "/admin/" )
663+ }
664+
665+ /// Main navigation hub handler
666+ async fn navigation_hub ( ) -> impl IntoResponse {
667+ Html ( generate_navigation_html ( ) )
668+ }
669+
670+ /// Generate the main navigation hub HTML
671+ fn generate_navigation_html ( ) -> String {
672+ r##"<!DOCTYPE html>
673+ <html lang="en">
674+ <head>
675+ <meta charset="UTF-8">
676+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
677+ <title>Loxone MCP Server - Navigation Hub</title>
678+ <style>
679+ :root {
680+ --loxone-green: #7aba00;
681+ --loxone-dark: #1a1a1a;
682+ --bg-primary: #0f0f0f;
683+ --bg-secondary: #1a1a1a;
684+ --bg-card: #252525;
685+ --text-primary: #e0e0e0;
686+ --text-secondary: #a0a0a0;
687+ --border-color: #333;
688+ --rust-orange: #ce422b;
689+ --success: #28a745;
690+ --info: #17a2b8;
691+ --warning: #ffc107;
692+ }
693+
694+ * {
695+ margin: 0;
696+ padding: 0;
697+ box-sizing: border-box;
698+ }
699+
700+ body {
701+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
702+ background: var(--bg-primary);
703+ color: var(--text-primary);
704+ line-height: 1.6;
705+ min-height: 100vh;
706+ }
707+
708+ .container {
709+ max-width: 1200px;
710+ margin: 0 auto;
711+ padding: 2rem;
712+ }
713+
714+ .header {
715+ text-align: center;
716+ margin-bottom: 3rem;
717+ padding-bottom: 2rem;
718+ border-bottom: 2px solid var(--border-color);
719+ }
720+
721+ h1 {
722+ color: var(--loxone-green);
723+ font-size: 2.5rem;
724+ margin-bottom: 0.5rem;
725+ }
726+
727+ .subtitle {
728+ color: var(--text-secondary);
729+ font-size: 1.1rem;
730+ }
731+
732+ .grid {
733+ display: grid;
734+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
735+ gap: 2rem;
736+ margin-top: 2rem;
737+ }
738+
739+ .category {
740+ background: var(--bg-card);
741+ border: 1px solid var(--border-color);
742+ border-radius: 12px;
743+ padding: 1.5rem;
744+ transition: all 0.3s ease;
745+ }
746+
747+ .category:hover {
748+ border-color: var(--loxone-green);
749+ box-shadow: 0 4px 12px rgba(122, 186, 0, 0.1);
750+ }
751+
752+ .category-title {
753+ font-size: 1.3rem;
754+ color: var(--loxone-green);
755+ margin-bottom: 1rem;
756+ display: flex;
757+ align-items: center;
758+ gap: 0.5rem;
759+ }
760+
761+ .link-grid {
762+ display: flex;
763+ flex-direction: column;
764+ gap: 0.75rem;
765+ }
766+
767+ .nav-link {
768+ display: flex;
769+ align-items: center;
770+ gap: 0.75rem;
771+ padding: 0.75rem 1rem;
772+ background: var(--bg-secondary);
773+ border: 1px solid var(--border-color);
774+ border-radius: 8px;
775+ color: var(--text-primary);
776+ text-decoration: none;
777+ transition: all 0.3s ease;
778+ cursor: pointer;
779+ }
780+
781+ .nav-link:hover {
782+ background: var(--bg-primary);
783+ border-color: var(--loxone-green);
784+ color: var(--loxone-green);
785+ transform: translateY(-2px);
786+ }
787+
788+ .link-icon {
789+ font-size: 1.2rem;
790+ width: 24px;
791+ text-align: center;
792+ }
793+
794+ .link-details {
795+ flex: 1;
796+ }
797+
798+ .link-title {
799+ font-weight: 500;
800+ margin-bottom: 0.25rem;
801+ }
802+
803+ .link-description {
804+ font-size: 0.875rem;
805+ color: var(--text-secondary);
806+ }
807+
808+ .status-indicator {
809+ width: 8px;
810+ height: 8px;
811+ border-radius: 50%;
812+ background: var(--success);
813+ }
814+
815+ .footer {
816+ margin-top: 3rem;
817+ padding-top: 2rem;
818+ border-top: 1px solid var(--border-color);
819+ text-align: center;
820+ color: var(--text-secondary);
821+ }
822+
823+ .api-key-display {
824+ background: var(--bg-secondary);
825+ border: 1px solid var(--border-color);
826+ border-radius: 8px;
827+ padding: 1rem;
828+ margin: 1rem 0;
829+ font-family: 'Courier New', monospace;
830+ font-size: 0.9rem;
831+ word-break: break-all;
832+ }
833+ </style>
834+ </head>
835+ <body>
836+ <div class="container">
837+ <div class="header">
838+ <h1>🏠 Loxone MCP Server</h1>
839+ <div class="subtitle">Navigation Hub - Access all interfaces and tools</div>
840+ <div id="apiKeyDisplay" class="api-key-display" style="display: none;">
841+ Using API Key: <span id="apiKeyValue"></span>
842+ </div>
843+ </div>
844+
845+ <div class="grid">
846+ <!-- Administration -->
847+ <div class="category">
848+ <div class="category-title">
849+ 🔧 Administration
850+ </div>
851+ <div class="link-grid">
852+ <a href="#" class="nav-link" onclick="navigateTo('/admin/keys')">
853+ <span class="link-icon">🔑</span>
854+ <div class="link-details">
855+ <div class="link-title">API Key Management</div>
856+ <div class="link-description">Generate, edit, and manage API keys</div>
857+ </div>
858+ <div class="status-indicator"></div>
859+ </a>
860+ <a href="#" class="nav-link" onclick="navigateTo('/admin/status')">
861+ <span class="link-icon">📊</span>
862+ <div class="link-details">
863+ <div class="link-title">Server Status</div>
864+ <div class="link-description">View server health and statistics</div>
865+ </div>
866+ <div class="status-indicator"></div>
867+ </a>
868+ <a href="#" class="nav-link" onclick="navigateTo('/admin/rate-limits')">
869+ <span class="link-icon">⚡</span>
870+ <div class="link-details">
871+ <div class="link-title">Rate Limits</div>
872+ <div class="link-description">Monitor API rate limiting status</div>
873+ </div>
874+ <div class="status-indicator"></div>
875+ </a>
876+ </div>
877+ </div>
878+
879+ <!-- Monitoring & Dashboards -->
880+ <div class="category">
881+ <div class="category-title">
882+ 📈 Monitoring & Dashboards
883+ </div>
884+ <div class="link-grid">
885+ <a href="#" class="nav-link" onclick="navigateTo('/dashboard/')">
886+ <span class="link-icon">🎛️</span>
887+ <div class="link-details">
888+ <div class="link-title">Unified Dashboard</div>
889+ <div class="link-description">Real-time Loxone system overview</div>
890+ </div>
891+ <div class="status-indicator"></div>
892+ </a>
893+ <a href="#" class="nav-link" onclick="navigateTo('/history/')">
894+ <span class="link-icon">📜</span>
895+ <div class="link-details">
896+ <div class="link-title">History Dashboard</div>
897+ <div class="link-description">Historical data and trends</div>
898+ </div>
899+ <div class="status-indicator"></div>
900+ </a>
901+ <a href="#" class="nav-link" onclick="navigateTo('/metrics')">
902+ <span class="link-icon">📊</span>
903+ <div class="link-details">
904+ <div class="link-title">Prometheus Metrics</div>
905+ <div class="link-description">Raw metrics for monitoring tools</div>
906+ </div>
907+ <div class="status-indicator"></div>
908+ </a>
909+ </div>
910+ </div>
911+
912+ <!-- System Health -->
913+ <div class="category">
914+ <div class="category-title">
915+ 💚 System Health
916+ </div>
917+ <div class="link-grid">
918+ <a href="#" class="nav-link" onclick="navigateTo('/health')">
919+ <span class="link-icon">❤️</span>
920+ <div class="link-details">
921+ <div class="link-title">Health Check</div>
922+ <div class="link-description">Basic system health status</div>
923+ </div>
924+ <div class="status-indicator"></div>
925+ </a>
926+ <a href="#" class="nav-link" onclick="navigateTo('/')">
927+ <span class="link-icon">📋</span>
928+ <div class="link-details">
929+ <div class="link-title">Server Info</div>
930+ <div class="link-description">API endpoints and server information</div>
931+ </div>
932+ <div class="status-indicator"></div>
933+ </a>
934+ </div>
935+ </div>
936+
937+ <!-- MCP Tools -->
938+ <div class="category">
939+ <div class="category-title">
940+ 🔌 MCP Integration
941+ </div>
942+ <div class="link-grid">
943+ <a href="#" class="nav-link" onclick="navigateTo('/mcp/sse')">
944+ <span class="link-icon">🔄</span>
945+ <div class="link-details">
946+ <div class="link-title">MCP SSE Endpoint</div>
947+ <div class="link-description">Server-Sent Events for MCP clients</div>
948+ </div>
949+ <div class="status-indicator"></div>
950+ </a>
951+ <a href="#" class="nav-link" onclick="navigateTo('/mcp/info')">
952+ <span class="link-icon">ℹ️</span>
953+ <div class="link-details">
954+ <div class="link-title">MCP Information</div>
955+ <div class="link-description">Available tools and capabilities</div>
956+ </div>
957+ <div class="status-indicator"></div>
958+ </a>
959+ <a href="#" class="nav-link" onclick="navigateTo('/mcp/tools')">
960+ <span class="link-icon">🛠️</span>
961+ <div class="link-details">
962+ <div class="link-title">MCP Tools</div>
963+ <div class="link-description">List of available MCP tools</div>
964+ </div>
965+ <div class="status-indicator"></div>
966+ </a>
967+ </div>
968+ </div>
969+ </div>
970+
971+ <div class="footer">
972+ <p>🦀 Loxone MCP Server - Built with Rust</p>
973+ <p>Navigation Hub v1.0 - Browser-friendly interface</p>
974+ </div>
975+ </div>
976+
977+ <script>
978+ // Get API key from URL parameters
979+ function getApiKey() {
980+ const params = new URLSearchParams(window.location.search);
981+ return params.get('api_key');
982+ }
983+
984+ // Build URL with API key parameter
985+ function buildUrl(path) {
986+ const apiKey = getApiKey();
987+ if (apiKey) {
988+ const separator = path.includes('?') ? '&' : '?';
989+ return path + separator + 'api_key=' + encodeURIComponent(apiKey);
990+ }
991+ return path;
992+ }
993+
994+ // Navigate to a page with API key
995+ function navigateTo(path) {
996+ window.location.href = buildUrl(path);
997+ }
998+
999+ // Show API key if present
1000+ window.onload = function() {
1001+ const apiKey = getApiKey();
1002+ if (apiKey) {
1003+ document.getElementById('apiKeyDisplay').style.display = 'block';
1004+ document.getElementById('apiKeyValue').textContent = apiKey;
1005+ }
1006+ };
1007+ </script>
1008+ </body>
1009+ </html>"##
1010+ . to_string ( )
1011+ }
1012+
6471013/// Root handler
6481014async fn root_handler ( ) -> impl IntoResponse {
6491015 Json ( serde_json:: json!( {
0 commit comments