Sfoglia il codice sorgente

feat(routing): introduce dedicated Routing sidebar category and separate BGP and OSPF sections with subtabs

Proxmox Router 1 settimana fa
parent
commit
fafb142f46
2 ha cambiato i file con 205 aggiunte e 41 eliminazioni
  1. 55 7
      internal/web/static/app.js
  2. 150 34
      internal/web/static/index.html

+ 55 - 7
internal/web/static/app.js

@@ -60,6 +60,11 @@ document.addEventListener('DOMContentLoaded', () => {
 
 // Navigation Handler
 function navigate(tabName) {
+  if (tabName === 'dynamic-routing' || tabName === 'routing') {
+    navigate('bgp');
+    return;
+  }
+
   state.activeTab = tabName;
   window.location.hash = tabName;
 
@@ -81,7 +86,9 @@ function navigate(tabName) {
     vlans: { title: 'VLAN Virtual Networks', sub: 'IEEE 802.1Q sub-interface segmentation and routing' },
     firewall: { title: 'MikroTik Firewall', sub: 'Filter rules, NAT, Mangle, Address Lists & Active Connections tracking' },
     routes: { title: 'Routing & Policy', sub: 'Routing tables, static routes (DAS/DAC), and policy routing rules' },
-    'dynamic-routing': { title: 'Dynamic Routing (BGP & OSPF)', sub: 'FRRouting BGP peering sessions, advertised networks, and OSPFv2 link-state routing' },
+    bgp: { title: 'BGP Routing (/routing bgp)', sub: 'FRRouting BGP peering sessions, advertised networks, and BGP routing table (RIB)' },
+    ospf: { title: 'OSPF Routing (/routing ospf)', sub: 'FRRouting OSPFv2 link-state routing, dynamic neighbor adjacencies, and multi-area topology' },
+    'dynamic-routing': { title: 'BGP Routing (/routing bgp)', sub: 'FRRouting dynamic routing' },
     ppp: { title: 'PPP Servers & Secrets', sub: 'MikroTik RouterOS user accounts, IP pools and VPN server concentrators' },
     eoip: { title: 'EoIP Tunnels', sub: 'Ethernet over IP Layer 2 tunneling (MikroTik RouterOS style)' },
     pppoe: { title: 'PPPoE Client WAN', sub: 'Point-to-Point Protocol over Ethernet WAN access' },
@@ -110,6 +117,14 @@ function navigate(tabName) {
   if (tabName === 'bridge') loadBridge();
   if (tabName === 'firewall') loadFirewall();
   if (tabName === 'routes') loadRoutes();
+  if (tabName === 'bgp') {
+    loadDynamicRouting();
+    switchBgpSubTab(state.activeBgpSubTab || 'bgp-peers');
+  }
+  if (tabName === 'ospf') {
+    loadDynamicRouting();
+    switchOspfSubTab(state.activeOspfSubTab || 'ospf-ifaces');
+  }
   if (tabName === 'dynamic-routing') loadDynamicRouting();
   if (tabName === 'ppp') loadPPP();
   if (tabName === 'eoip') loadEoIP();
@@ -175,7 +190,7 @@ async function fetchData(isManual = false) {
     if (state.activeTab === 'vlans') renderVlansList();
     if (state.activeTab === 'firewall') loadFirewall(false);
     if (state.activeTab === 'routes') loadRoutes(false);
-    if (state.activeTab === 'dynamic-routing') loadDynamicRouting(false);
+    if (state.activeTab === 'dynamic-routing' || state.activeTab === 'bgp' || state.activeTab === 'ospf') loadDynamicRouting(false);
     if (state.activeTab === 'ppp') loadPPP(false);
     if (state.activeTab === 'eoip') loadEoIP(false);
     if (state.activeTab === 'pppoe') loadPPPoE(false);
@@ -301,6 +316,10 @@ function updateBadges() {
     const d = state.dynSummary;
     setDotBadge('badge-bgp-dot', d.bgp_running);
     setDotBadge('badge-ospf-dot', d.ospf_running);
+    const bBgp = document.getElementById('badge-bgp-count');
+    if (bBgp) bBgp.innerText = (state.bgpPeers || []).length;
+    const bOspf = document.getElementById('badge-ospf-count');
+    if (bOspf) bOspf.innerText = (state.ospfInterfaces || []).length;
   }
 }
 
@@ -5037,6 +5056,8 @@ function downloadWGClientConf() {
 // =========================================================================
 
 state.activeDynSubTab = 'bgp-peers';
+state.activeBgpSubTab = 'bgp-peers';
+state.activeOspfSubTab = 'ospf-ifaces';
 state.bgpInstance = null;
 state.bgpPeers = [];
 state.bgpNets = [];
@@ -5047,16 +5068,33 @@ state.ospfAreas = [];
 state.ospfNeighbors = [];
 state.ospfRoutes = [];
 
-function switchDynSubTab(subTab) {
-  state.activeDynSubTab = subTab;
-  const tabs = ['bgp-peers', 'bgp-nets', 'bgp-routes', 'ospf-ifaces', 'ospf-areas', 'ospf-nbrs', 'ospf-routes'];
+function switchBgpSubTab(subTab) {
+  state.activeBgpSubTab = subTab;
+  const tabs = ['bgp-peers', 'bgp-nets', 'bgp-routes'];
+  tabs.forEach(t => {
+    const el = document.getElementById(`dyn-subtab-${t}`);
+    const btn = document.getElementById(`bgp-subtab-btn-${t}`);
+    if (el) el.classList.toggle('hidden', t !== subTab);
+    if (btn) {
+      if (t === subTab) {
+        btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-purple-500 text-white transition flex items-center gap-2 whitespace-nowrap';
+      } else {
+        btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap';
+      }
+    }
+  });
+}
+
+function switchOspfSubTab(subTab) {
+  state.activeOspfSubTab = subTab;
+  const tabs = ['ospf-ifaces', 'ospf-areas', 'ospf-nbrs', 'ospf-routes'];
   tabs.forEach(t => {
     const el = document.getElementById(`dyn-subtab-${t}`);
-    const btn = document.getElementById(`dyn-subtab-btn-${t}`);
+    const btn = document.getElementById(`ospf-subtab-btn-${t}`);
     if (el) el.classList.toggle('hidden', t !== subTab);
     if (btn) {
       if (t === subTab) {
-        btn.className = `px-4 py-2.5 text-xs font-bold border-b-2 ${t.startsWith('bgp') ? 'border-purple-500' : 'border-indigo-500'} text-white transition flex items-center gap-2 whitespace-nowrap`;
+        btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-indigo-500 text-white transition flex items-center gap-2 whitespace-nowrap';
       } else {
         btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap';
       }
@@ -5064,6 +5102,16 @@ function switchDynSubTab(subTab) {
   });
 }
 
+function switchDynSubTab(subTab) {
+  if (subTab.startsWith('bgp')) {
+    if (state.activeTab !== 'bgp') navigate('bgp');
+    switchBgpSubTab(subTab);
+  } else if (subTab.startsWith('ospf')) {
+    if (state.activeTab !== 'ospf') navigate('ospf');
+    switchOspfSubTab(subTab);
+  }
+}
+
 async function loadDynamicRouting(showToastMsg = true) {
   try {
     const [sumRes, bgpInstRes, bgpPeersRes, bgpNetsRes, bgpRoutesRes, ospfInstRes, ospfIfacesRes, ospfAreasRes, ospfNbrsRes, ospfRoutesRes] = await Promise.all([

+ 150 - 34
internal/web/static/index.html

@@ -135,6 +135,9 @@
           <span id="badge-firewall-count" class="text-[10px] bg-emerald-950 text-emerald-400 border border-emerald-800/50 px-1.5 py-0.2 rounded-full font-mono">0</span>
         </a>
 
+        <!-- CATEGORY: ROUTING -->
+        <div class="px-3 pt-3 pb-1 text-[10px] font-bold text-slate-500 uppercase tracking-wider">Routing</div>
+
         <a href="#routes" onclick="navigate('routes')" id="nav-routes"
            class="nav-link flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium transition-all group text-slate-300 hover:bg-slate-800/80 hover:text-white">
           <div class="flex items-center space-x-2.5">
@@ -146,17 +149,31 @@
           <span id="badge-routes-count" class="text-[10px] bg-slate-800 text-slate-300 px-1.5 py-0.2 rounded-full font-mono">0</span>
         </a>
 
-        <a href="#dynamic-routing" onclick="navigate('dynamic-routing')" id="nav-dynamic-routing"
+        <a href="#bgp" onclick="navigate('bgp')" id="nav-bgp"
            class="nav-link flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium transition-all group text-slate-300 hover:bg-slate-800/80 hover:text-white">
           <div class="flex items-center space-x-2.5">
             <svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
               <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
             </svg>
-            <span>BGP & OSPF</span>
+            <span>BGP (/routing bgp)</span>
           </div>
           <div class="flex items-center gap-1.5">
             <span id="badge-bgp-dot" class="w-2 h-2 rounded-full bg-slate-600" title="BGP Status"></span>
+            <span id="badge-bgp-count" class="text-[10px] bg-purple-950 text-purple-300 border border-purple-800/40 px-1.5 py-0.2 rounded-full font-mono">0</span>
+          </div>
+        </a>
+
+        <a href="#ospf" onclick="navigate('ospf')" id="nav-ospf"
+           class="nav-link flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium transition-all group text-slate-300 hover:bg-slate-800/80 hover:text-white">
+          <div class="flex items-center space-x-2.5">
+            <svg class="w-4 h-4 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
+            </svg>
+            <span>OSPF (/routing ospf)</span>
+          </div>
+          <div class="flex items-center gap-1.5">
             <span id="badge-ospf-dot" class="w-2 h-2 rounded-full bg-slate-600" title="OSPF Status"></span>
+            <span id="badge-ospf-count" class="text-[10px] bg-indigo-950 text-indigo-300 border border-indigo-800/40 px-1.5 py-0.2 rounded-full font-mono">0</span>
           </div>
         </a>
 
@@ -1754,6 +1771,28 @@
 
         <!-- ================= SECTION: ROUTES & POLICY (/ip route & rules) ================= -->
         <section id="section-routes" class="hidden space-y-6">
+          <!-- Top Routing Switcher Bar -->
+          <div class="flex items-center gap-1.5 p-1 bg-slate-900/90 border border-slate-800 rounded-xl w-fit">
+            <button onclick="navigate('routes')" class="px-3.5 py-1.5 text-xs font-bold rounded-lg transition bg-cyan-600/20 text-cyan-300 border border-cyan-500/40 shadow-sm flex items-center gap-2">
+              <svg class="w-3.5 h-3.5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
+              </svg>
+              <span>Routes & Policy (/ip route)</span>
+            </button>
+            <button onclick="navigate('bgp')" class="px-3.5 py-1.5 text-xs font-medium rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition flex items-center gap-2">
+              <svg class="w-3.5 h-3.5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
+              </svg>
+              <span>BGP (/routing bgp)</span>
+            </button>
+            <button onclick="navigate('ospf')" class="px-3.5 py-1.5 text-xs font-medium rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition flex items-center gap-2">
+              <svg class="w-3.5 h-3.5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
+              </svg>
+              <span>OSPF (/routing ospf)</span>
+            </button>
+          </div>
+
           <div class="bg-gradient-to-r from-cyan-950/40 via-slate-900 to-slate-900 border border-cyan-900/30 rounded-xl p-5 flex flex-wrap items-center justify-between gap-4">
             <div>
               <div class="flex items-center gap-2 mb-1">
@@ -1883,64 +1922,70 @@
           </div>
         </section>
 
-        <!-- ================= SECTION: DYNAMIC ROUTING (BGP & OSPF) ================= -->
-        <section id="section-dynamic-routing" class="hidden space-y-6">
+        <!-- ================= SECTION: BGP ROUTING (/routing bgp) ================= -->
+        <section id="section-bgp" class="hidden space-y-6">
+          <!-- Top Routing Switcher Bar -->
+          <div class="flex items-center gap-1.5 p-1 bg-slate-900/90 border border-slate-800 rounded-xl w-fit">
+            <button onclick="navigate('routes')" class="px-3.5 py-1.5 text-xs font-medium rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition flex items-center gap-2">
+              <svg class="w-3.5 h-3.5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
+              </svg>
+              <span>Routes & Policy (/ip route)</span>
+            </button>
+            <button onclick="navigate('bgp')" class="px-3.5 py-1.5 text-xs font-bold rounded-lg transition bg-purple-600/20 text-purple-300 border border-purple-500/40 shadow-sm flex items-center gap-2">
+              <svg class="w-3.5 h-3.5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
+              </svg>
+              <span>BGP (/routing bgp)</span>
+            </button>
+            <button onclick="navigate('ospf')" class="px-3.5 py-1.5 text-xs font-medium rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition flex items-center gap-2">
+              <svg class="w-3.5 h-3.5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
+              </svg>
+              <span>OSPF (/routing ospf)</span>
+            </button>
+          </div>
+
           <div class="bg-gradient-to-r from-purple-950/40 via-slate-900 to-slate-900 border border-purple-900/30 rounded-xl p-5 flex flex-wrap items-center justify-between gap-4">
             <div>
               <div class="flex items-center gap-2 mb-1">
                 <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-purple-500/20 text-purple-300 border border-purple-500/30 font-semibold">/routing bgp</span>
-                <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 font-semibold">/routing ospf</span>
-                <h2 class="text-base font-bold text-white">Dynamic Routing Engine (BGP & OSPF)</h2>
+                <h2 class="text-base font-bold text-white">BGP Dynamic Routing</h2>
+                <span id="bgp-live-status-pill" class="text-[10px] px-2 py-0.5 rounded font-mono bg-slate-800 text-slate-400 border border-slate-700">Checking...</span>
               </div>
-              <p class="text-xs text-slate-400">Enterprise FRRouting stack providing BGP peering, network advertisement, and OSPFv2 link-state routing.</p>
+              <p class="text-xs text-slate-400">Enterprise FRRouting BGP stack: Autonomous System peering sessions, prefix announcements, and BGP routing table (RIB).</p>
             </div>
 
             <div class="flex items-center space-x-2 flex-wrap gap-y-2">
               <button onclick="openBgpInstanceModal()" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-purple-300 border border-purple-900/50 transition flex items-center gap-1.5">
                 <span>⚙ BGP Settings</span>
               </button>
-              <button onclick="openOspfInstanceModal()" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-indigo-300 border border-indigo-900/50 transition flex items-center gap-1.5">
-                <span>⚙ OSPF Settings</span>
-              </button>
               <button onclick="openAddBgpPeerModal()" class="text-xs font-semibold px-3 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-white shadow-lg shadow-purple-500/20 transition">
                 + Add BGP Peer
               </button>
-              <button onclick="openAddOspfInterfaceModal()" class="text-xs font-semibold px-3 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white shadow-lg shadow-indigo-500/20 transition">
-                + Add OSPF Interface
+              <button onclick="openAddBgpNetworkModal()" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 transition">
+                + Add Network
+              </button>
+              <button onclick="loadDynamicRouting()" class="text-xs text-slate-400 hover:text-white px-2.5 py-2 bg-slate-800 rounded-lg border border-slate-700 transition">
+                Refresh
               </button>
             </div>
           </div>
 
-          <!-- Dynamic Routing Sub-Tabs -->
+          <!-- BGP Sub-Tabs -->
           <div class="flex border-b border-slate-800 space-x-1 overflow-x-auto">
-            <button id="dyn-subtab-btn-bgp-peers" onclick="switchDynSubTab('bgp-peers')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-purple-500 text-white transition flex items-center gap-2 whitespace-nowrap">
+            <button id="bgp-subtab-btn-bgp-peers" onclick="switchBgpSubTab('bgp-peers')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-purple-500 text-white transition flex items-center gap-2 whitespace-nowrap">
               <span>BGP Peers (/routing bgp connection)</span>
               <span id="dyn-badge-bgp-peers" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
             </button>
-            <button id="dyn-subtab-btn-bgp-nets" onclick="switchDynSubTab('bgp-nets')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
-              <span>BGP Networks</span>
+            <button id="bgp-subtab-btn-bgp-nets" onclick="switchBgpSubTab('bgp-nets')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>Advertised Networks (/routing bgp network)</span>
               <span id="dyn-badge-bgp-nets" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
             </button>
-            <button id="dyn-subtab-btn-bgp-routes" onclick="switchDynSubTab('bgp-routes')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+            <button id="bgp-subtab-btn-bgp-routes" onclick="switchBgpSubTab('bgp-routes')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
               <span>BGP Routes (RIB)</span>
               <span id="dyn-badge-bgp-routes" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
             </button>
-            <button id="dyn-subtab-btn-ospf-ifaces" onclick="switchDynSubTab('ospf-ifaces')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
-              <span>OSPF Interfaces</span>
-              <span id="dyn-badge-ospf-ifaces" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
-            </button>
-            <button id="dyn-subtab-btn-ospf-areas" onclick="switchDynSubTab('ospf-areas')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
-              <span>OSPF Areas</span>
-              <span id="dyn-badge-ospf-areas" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
-            </button>
-            <button id="dyn-subtab-btn-ospf-nbrs" onclick="switchDynSubTab('ospf-nbrs')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
-              <span>OSPF Neighbors</span>
-              <span id="dyn-badge-ospf-nbrs" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
-            </button>
-            <button id="dyn-subtab-btn-ospf-routes" onclick="switchDynSubTab('ospf-routes')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
-              <span>OSPF Routes</span>
-              <span id="dyn-badge-ospf-routes" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
-            </button>
           </div>
 
           <!-- Sub-Tab 1: BGP Peers -->
@@ -2035,6 +2080,77 @@
               No active routes in BGP table. Check peer connections and ensure BGP is enabled.
             </div>
           </div>
+        </section>
+
+        <!-- ================= SECTION: OSPF ROUTING (/routing ospf) ================= -->
+        <section id="section-ospf" class="hidden space-y-6">
+          <!-- Top Routing Switcher Bar -->
+          <div class="flex items-center gap-1.5 p-1 bg-slate-900/90 border border-slate-800 rounded-xl w-fit">
+            <button onclick="navigate('routes')" class="px-3.5 py-1.5 text-xs font-medium rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition flex items-center gap-2">
+              <svg class="w-3.5 h-3.5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
+              </svg>
+              <span>Routes & Policy (/ip route)</span>
+            </button>
+            <button onclick="navigate('bgp')" class="px-3.5 py-1.5 text-xs font-medium rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition flex items-center gap-2">
+              <svg class="w-3.5 h-3.5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
+              </svg>
+              <span>BGP (/routing bgp)</span>
+            </button>
+            <button onclick="navigate('ospf')" class="px-3.5 py-1.5 text-xs font-bold rounded-lg transition bg-indigo-600/20 text-indigo-300 border border-indigo-500/40 shadow-sm flex items-center gap-2">
+              <svg class="w-3.5 h-3.5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
+              </svg>
+              <span>OSPF (/routing ospf)</span>
+            </button>
+          </div>
+
+          <div class="bg-gradient-to-r from-indigo-950/40 via-slate-900 to-slate-900 border border-indigo-900/30 rounded-xl p-5 flex flex-wrap items-center justify-between gap-4">
+            <div>
+              <div class="flex items-center gap-2 mb-1">
+                <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 font-semibold">/routing ospf</span>
+                <h2 class="text-base font-bold text-white">OSPF Dynamic Routing</h2>
+                <span id="ospf-live-status-pill" class="text-[10px] px-2 py-0.5 rounded font-mono bg-slate-800 text-slate-400 border border-slate-700">Checking...</span>
+              </div>
+              <p class="text-xs text-slate-400">Enterprise FRRouting OSPFv2 stack: Link-state interior gateway routing, multi-area topologies, and neighbor adjacencies.</p>
+            </div>
+
+            <div class="flex items-center space-x-2 flex-wrap gap-y-2">
+              <button onclick="openOspfInstanceModal()" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-indigo-300 border border-indigo-900/50 transition flex items-center gap-1.5">
+                <span>⚙ OSPF Settings</span>
+              </button>
+              <button onclick="openAddOspfInterfaceModal()" class="text-xs font-semibold px-3 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white shadow-lg shadow-indigo-500/20 transition">
+                + Add OSPF Interface
+              </button>
+              <button onclick="openAddOspfAreaModal()" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 transition">
+                + Add Area
+              </button>
+              <button onclick="loadDynamicRouting()" class="text-xs text-slate-400 hover:text-white px-2.5 py-2 bg-slate-800 rounded-lg border border-slate-700 transition">
+                Refresh
+              </button>
+            </div>
+          </div>
+
+          <!-- OSPF Sub-Tabs -->
+          <div class="flex border-b border-slate-800 space-x-1 overflow-x-auto">
+            <button id="ospf-subtab-btn-ospf-ifaces" onclick="switchOspfSubTab('ospf-ifaces')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-indigo-500 text-white transition flex items-center gap-2 whitespace-nowrap">
+              <span>OSPF Interfaces</span>
+              <span id="dyn-badge-ospf-ifaces" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
+            </button>
+            <button id="ospf-subtab-btn-ospf-areas" onclick="switchOspfSubTab('ospf-areas')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>OSPF Areas</span>
+              <span id="dyn-badge-ospf-areas" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
+            </button>
+            <button id="ospf-subtab-btn-ospf-nbrs" onclick="switchOspfSubTab('ospf-nbrs')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>OSPF Neighbors</span>
+              <span id="dyn-badge-ospf-nbrs" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
+            </button>
+            <button id="ospf-subtab-btn-ospf-routes" onclick="switchOspfSubTab('ospf-routes')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>OSPF Routes</span>
+              <span id="dyn-badge-ospf-routes" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
+            </button>
+          </div>
 
           <!-- Sub-Tab 4: OSPF Interfaces -->
           <div id="dyn-subtab-ospf-ifaces" class="hidden bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
@@ -4252,6 +4368,6 @@
     </div>
   </div>
 
-  <script src="app.js?v=3.1"></script>
+  <script src="app.js?v=3.2"></script>
 </body>
 </html>