--- examples/edge_explorer.html	2026-03-09 10:38:38.935553240 +0100
+++ src/radical/edge/data/edge_explorer.html	2026-03-04 17:26:34.370954468 +0100
@@ -32,11 +32,16 @@
       --card-r: 12px;
     }
 
+    html, body {
+      height: 100%;
+      margin: 0;
+      overflow: hidden; /* Prevent body scrolling - children scroll independently */
+    }
+
     body {
       font-family: 'Inter', sans-serif;
       background: var(--bg);
       color: var(--text);
-      min-height: 100vh;
       display: flex;
       flex-direction: column;
     }
@@ -300,15 +305,44 @@
     .shell {
       display: flex;
       flex: 1;
+      overflow: hidden; /* Prevent shell from scrolling, children scroll independently */
+      min-height: 0; /* Required for flex children to scroll properly */
     }
 
     /* ── sidebar ─────────────────────────────────────────── */
     aside {
       width: 240px;
+      min-width: 160px;
+      max-width: 50vw;
       background: var(--bg2);
       border-right: 1px solid var(--border);
       padding: 20px 0;
       flex-shrink: 0;
+      overflow-y: auto;
+      overflow-x: hidden;
+      min-height: 0; /* Required for flex child to scroll properly */
+      /* Hide scrollbar but keep functionality */
+      scrollbar-width: none; /* Firefox */
+      -ms-overflow-style: none; /* IE/Edge */
+      position: relative;
+    }
+    aside::-webkit-scrollbar {
+      display: none; /* Chrome/Safari/Opera */
+    }
+
+    .sidebar-resize-handle {
+      position: absolute;
+      top: 0;
+      right: 0;
+      width: 5px;
+      height: 100%;
+      cursor: ew-resize;
+      background: transparent;
+      transition: background 0.2s;
+    }
+    .sidebar-resize-handle:hover,
+    .sidebar-resize-handle.dragging {
+      background: var(--accent);
     }
 
     .section-label {
@@ -333,7 +367,6 @@
       font-size: .85rem;
       border-radius: 0;
       transition: background .15s;
-      border-left: 3px solid transparent;
     }
 
     .tree-item:hover {
@@ -342,23 +375,19 @@
 
     .tree-item.active {
       background: rgba(108, 99, 255, .12);
-      border-left-color: var(--accent);
+      font-weight: 700;
       color: #a5b4fc;
     }
 
-    .tree-item.edge {
-      font-weight: 600;
-      color: var(--accent2);
-    }
-
     .tree-item.plugin {
-      padding-left: 32px;
+      padding-left: 20px;
       color: var(--muted);
       font-size: .8rem;
     }
 
     .tree-item.plugin.active {
       color: #a5b4fc;
+      font-weight: 700;
     }
 
     .icon {
@@ -370,6 +399,7 @@
       flex: 1;
       padding: 28px 32px;
       overflow-y: auto;
+      min-height: 0; /* Required for flex child to scroll properly */
     }
 
     .page {
@@ -599,6 +629,11 @@
       color: var(--accent);
     }
 
+    /* Sysinfo needs more space - no max-height */
+    .monitor-area.sysinfo-content {
+      max-height: none;
+    }
+
     /* task entries in job monitor */
     .task-entry {
       margin: 0;
@@ -888,7 +923,7 @@
         <div id="statusDot" class="status-dot" title="Not connected"></div>
         <span id="statusText">Disconnected</span>
       </div>
-      <button class="btn btn-primary" onclick="doConnect()" id="connectBtn">Connect</button>
+      <button class="btn btn-primary" onclick="toggleConnection()" id="connectBtn">Connect</button>
     </div>
   </header>
 
@@ -897,15 +932,12 @@
     <!-- ── SIDEBAR ──────────────────────────────────────────── -->
     <aside id="sidebar">
       <ul class="tree" id="treeRoot">
-        <li class="section-label">Navigation</li>
-        <li class="tree-item active" id="nav-welcome" onclick="showPage('welcome')">
-          <span class="icon">🏠</span> Welcome
-        </li>
-        <li class="tree-item" id="nav-hierarchy" onclick="showPage('hierarchy')">
-          <span class="icon">🔗</span> Endpoint Hierarchy
+        <li class="tree-item" id="nav-bridge" onclick="showPage('bridge')" style="display:none;">
+          <span class="icon">🌉</span> <span id="bridgeLabel">Bridge</span>
         </li>
       </ul>
       <div id="edgeTree"></div>
+      <div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
     </aside>
 
     <!-- ── MAIN ────────────────────────────────────────────── -->
@@ -937,44 +969,17 @@
               </li>
             </ol>
           </div>
-          <div class="feature-grid">
-            <div class="feature-card">
-              <div class="fc-icon">🗂️</div>
-              <h4>Endpoint Hierarchy</h4>
-              <p>Browse all connected edges and their registered plugins.</p>
-            </div>
-            <div class="feature-card">
-              <div class="fc-icon">🖥️</div>
-              <h4>System Info</h4>
-              <p>Live CPU, memory, disk, network and GPU metrics.</p>
-            </div>
-            <div class="feature-card">
-              <div class="fc-icon">📋</div>
-              <h4>Queue Info</h4>
-              <p>Inspect Slurm partitions, jobs and allocations.</p>
-            </div>
-            <div class="feature-card">
-              <div class="fc-icon">🚀</div>
-              <h4>PsiJ Jobs</h4>
-              <p>Submit and monitor HPC batch jobs via PsiJ.</p>
-            </div>
-            <div class="feature-card">
-              <div class="fc-icon">🎼</div>
-              <h4>Rhapsody Tasks</h4>
-              <p>Submit compute tasks, wait for results, view stdout/stderr.</p>
-            </div>
-          </div>
         </div>
       </div>
 
-      <!-- Hierarchy page -->
-      <div class="page" id="page-hierarchy">
+      <!-- Bridge info page -->
+      <div class="page" id="page-bridge">
         <div class="page-header">
-          <div class="page-icon">🔗</div>
-          <h2>Endpoint Hierarchy</h2>
+          <div class="page-icon">🌉</div>
+          <h2 id="bridgePageTitle">Bridge</h2>
           <button class="btn btn-secondary btn-sm" style="margin-left:auto" onclick="loadHierarchy()">↺ Refresh</button>
         </div>
-        <div id="hierarchyContent">
+        <div id="bridgeInfoContent">
           <div class="empty">
             <div class="empty-icon">🔌</div>
             <p>Not connected. Enter a Bridge URL and click Connect.</p>
@@ -1047,7 +1052,7 @@
           <div class="grid2">
             <div>
               <div class="form-group"><label>Executable</label><input class="p-exec" type="text"
-                  value="radical-edge-wrapper.sh" />
+                  value="radical-edge-service.py" />
               </div>
               <div class="form-group"><label>Arguments (space-separated)</label><input class="p-args" type="text"
                   value="" placeholder="auto-filled with --url and --name" /></div>
@@ -1069,13 +1074,6 @@
                   placeholder="e.g. 600" /></div>
             </div>
           </div>
-          
-          <div class="card-title" style="margin-top: 15px; font-size: 0.85rem;">🔧 Custom Attributes</div>
-          <div class="psij-attributes-container" style="margin-bottom: 14px;">
-            <div class="psij-attribute-rows"></div>
-            <button class="btn btn-secondary btn-sm" onclick="addPsijAttributeRow(this)" style="margin-top:8px;">➕ Add Attribute</button>
-          </div>
-          
           <button class="btn btn-success" onclick="submitPsijJob(this)">🚀 Submit Job</button>
         </div>
         <div class="card psij-jobs-card">
@@ -1126,7 +1124,6 @@
     let eventSource = null;
     const sessions = {};   // key: "edgeName/pluginName" -> sid
     const edgeData = {};   // edgeName -> plugin info from /edge/list
-    const queueDataCache = {}; // edgeName -> { queues: {...}, allocations: [...] }
 
     // Task registry: taskRegistry[edgeName][pluginName] = { id: {state, label, ...} }
     const taskRegistry = {};
@@ -1134,43 +1131,373 @@
     // Pending notifications for elements that don't exist yet (race condition fix)
     // pendingNotifications[uid] = { data, timestamp }
     const pendingNotifications = {};
+
+    // Pending state updates for tasks that haven't been registered yet (race condition fix)
+    // pendingStateUpdates[edgeName/plugin/id] = { state, timestamp }
+    const pendingStateUpdates = {};
+
     function registerTask(edgeName, plugin, id, label) {
       if (!taskRegistry[edgeName]) taskRegistry[edgeName] = {};
       if (!taskRegistry[edgeName][plugin]) taskRegistry[edgeName][plugin] = {};
-      taskRegistry[edgeName][plugin][id] = { state: 'SUBMITTED', label };
-      // Force keeping the new plugin details open
-      if (window.lastEdges) {
-        const open = new Set();
-        document.querySelectorAll('#edgeTree details[open]').forEach(el => { if (el.id) open.add(el.id); });
-        open.add(`sidebar-edge-${edgeName}`);
-        open.add(`sidebar-plugin-${edgeName}__${plugin}`);
-        buildSidebar(window.lastEdges);
-        document.querySelectorAll('#edgeTree details').forEach(el => { if (el.id && open.has(el.id)) el.open = true; });
-      }
+
+      // Check for pending state update that arrived before registration
+      const pendingKey = `${edgeName}/${plugin}/${id}`;
+      const pending = pendingStateUpdates[pendingKey];
+      const initialState = pending ? pending.state : 'SUBMITTED';
+      if (pending) delete pendingStateUpdates[pendingKey];
+
+      taskRegistry[edgeName][plugin][id] = { state: initialState, label };
+      if (window.lastEdges) rebuildSidebar();
     }
+
     function updateTaskState(edgeName, plugin, id, state) {
       if (taskRegistry[edgeName]?.[plugin]?.[id]) {
         taskRegistry[edgeName][plugin][id].state = state;
         rebuildSidebar();
-        console.log(`[updateTaskState] Updated ${edgeName}/${plugin}/${id} -> ${state}`);
       } else {
-        // Task not found - log diagnostic info
-        console.warn(`[updateTaskState] Task not found: edge=${edgeName} plugin=${plugin} id=${id}`);
-        console.warn(`[updateTaskState] Registered edges:`, Object.keys(taskRegistry));
-        if (taskRegistry[edgeName]) {
-          console.warn(`[updateTaskState] Plugins on ${edgeName}:`, Object.keys(taskRegistry[edgeName]));
-          if (taskRegistry[edgeName][plugin]) {
-            console.warn(`[updateTaskState] Tasks on ${edgeName}/${plugin}:`, Object.keys(taskRegistry[edgeName][plugin]));
-          }
-        }
+        // Task not registered yet - queue the state update
+        const pendingKey = `${edgeName}/${plugin}/${id}`;
+        pendingStateUpdates[pendingKey] = { state, timestamp: Date.now() };
       }
     }
+
+    // Debounced sidebar rebuild to prevent UI jank with rapid updates
+    let _rebuildSidebarTimer = null;
     function rebuildSidebar() {
+      if (_rebuildSidebarTimer) clearTimeout(_rebuildSidebarTimer);
+      _rebuildSidebarTimer = setTimeout(_doRebuildSidebar, 100);
+    }
+    function _doRebuildSidebar() {
       if (!window.lastEdges) return;
       const open = new Set();
       document.querySelectorAll('#edgeTree details[open]').forEach(el => { if (el.id) open.add(el.id); });
       buildSidebar(window.lastEdges);
-      document.querySelectorAll('#edgeTree details').forEach(el => { if (el.id && open.has(el.id)) el.open = true; });
+      // Restore open/closed state - first close all, then open those that were open
+      document.querySelectorAll('#edgeTree details').forEach(el => {
+        if (el.id) {
+          el.open = open.has(el.id);
+        }
+      });
+    }
+
+    // ─────────────────────────────────────────────────────────────
+    //  Dynamic UI Rendering from ui_config
+    // ─────────────────────────────────────────────────────────────
+
+    function getPluginUIConfig(edgeName, pluginName) {
+      return edgeData[edgeName]?.plugins?.[pluginName]?.ui_config || null;
+    }
+
+    function renderUIField(field) {
+      const required = field.required !== false;
+      const cssClass = field.css_class || `field-${field.name}`;
+      let input;
+
+      switch (field.type) {
+        case 'select':
+          const opts = (field.options || []).map(o =>
+            typeof o === 'string'
+              ? `<option value="${escHtml(o)}">${escHtml(o)}</option>`
+              : `<option value="${escHtml(o.value)}">${escHtml(o.label || o.value)}</option>`
+          ).join('');
+          input = `<select class="${cssClass}" ${required ? 'required' : ''}>${opts}</select>`;
+          break;
+        case 'textarea':
+          input = `<textarea class="${cssClass}" placeholder="${escHtml(field.placeholder || '')}" ${required ? 'required' : ''}>${escHtml(field.default || '')}</textarea>`;
+          break;
+        case 'number':
+          input = `<input class="${cssClass}" type="number" value="${escHtml(field.default || '')}" placeholder="${escHtml(field.placeholder || '')}" ${required ? 'required' : ''} />`;
+          break;
+        case 'checkbox':
+          const checked = field.default === 'true' || field.default === true ? 'checked' : '';
+          input = `<input class="${cssClass}" type="checkbox" ${checked} />`;
+          break;
+        default:
+          input = `<input class="${cssClass}" type="text" value="${escHtml(field.default || '')}" placeholder="${escHtml(field.placeholder || '')}" ${required ? 'required' : ''} />`;
+      }
+
+      return `<div class="form-group">
+        <label>${escHtml(field.label)}</label>
+        ${input}
+      </div>`;
+    }
+
+    /**
+     * Render a form card from ui_config form definition.
+     */
+    function renderUIForm(form, edgeName, pluginName) {
+      const layoutClass = form.layout === 'grid2' ? 'grid2' : form.layout === 'grid3' ? 'grid3' : '';
+
+      // Group fields by column if layout is grid
+      let fieldsHtml = '';
+      if (layoutClass && form.fields.some(f => f.column !== undefined)) {
+        // Multi-column layout
+        const columns = {};
+        form.fields.forEach(f => {
+          const col = f.column || 0;
+          if (!columns[col]) columns[col] = [];
+          columns[col].push(f);
+        });
+        const colCount = layoutClass === 'grid2' ? 2 : 3;
+        fieldsHtml = '<div class="' + layoutClass + '">';
+        for (let i = 0; i < colCount; i++) {
+          fieldsHtml += '<div>';
+          (columns[i] || []).forEach(f => { fieldsHtml += renderUIField(f); });
+          fieldsHtml += '</div>';
+        }
+        fieldsHtml += '</div>';
+      } else {
+        // Single column or simple layout
+        form.fields.forEach(f => { fieldsHtml += renderUIField(f); });
+      }
+
+      const submitStyle = form.submit?.style || 'success';
+      const submitLabel = form.submit?.label || 'Submit';
+      const formId = form.id || 'default';
+
+      return `<div class="card" data-form-id="${formId}">
+        <div class="card-title">${escHtml(form.title)}</div>
+        ${fieldsHtml}
+        <button class="btn btn-${submitStyle}" onclick="submitDynamicForm('${edgeName}', '${pluginName}', '${formId}', this)">
+          ${escHtml(submitLabel)}
+        </button>
+      </div>`;
+    }
+
+    function renderUIMonitor(monitor) {
+      const cssClass = monitor.css_class || `monitor-${monitor.id}`;
+      const emptyText = monitor.empty_text || '';
+
+      return `<div class="card ${monitor.id}-card">
+        <div class="card-title">${escHtml(monitor.title)}</div>
+        <div class="monitor-area ${cssClass}">${escHtml(emptyText)}</div>
+      </div>`;
+    }
+
+    function createDynamicPluginPage(edgeName, pluginName) {
+      const ui = getPluginUIConfig(edgeName, pluginName);
+      if (!ui || !ui.title) return null;
+
+      // Check for stub plugin
+      if (ui.stub_message) {
+        const page = document.createElement('div');
+        page.className = 'page';
+        page.innerHTML = `
+          <div class="page-header">
+            <div class="page-icon">${ui.icon || '🔌'}</div>
+            <h2>${escHtml(ui.title)} — <span class="edge-label">${edgeName}</span></h2>
+          </div>
+          <div class="card">
+            <p style="color:var(--muted)">${escHtml(ui.stub_message)}</p>
+          </div>`;
+        page.dataset.edgeName = edgeName;
+        page.dataset.pluginName = pluginName;
+        return page;
+      }
+
+      let html = `<div class="page-header">
+        <div class="page-icon">${ui.icon || '🔌'}</div>
+        <h2>${escHtml(ui.title)} — <span class="edge-label">${edgeName}</span></h2>
+        ${ui.refresh_button ? `<button class="btn btn-secondary btn-sm" style="margin-left:auto" onclick="refreshDynamicPage(this)">↺ Refresh</button>` : ''}
+      </div>`;
+
+      // Render forms
+      (ui.forms || []).forEach(form => {
+        html += renderUIForm(form, edgeName, pluginName);
+      });
+
+      // Render monitors
+      (ui.monitors || []).forEach(monitor => {
+        html += renderUIMonitor(monitor);
+      });
+
+      const page = document.createElement('div');
+      page.className = 'page';
+      page.innerHTML = html;
+      page.dataset.edgeName = edgeName;
+      page.dataset.pluginName = pluginName;
+      page.dataset.uiConfig = JSON.stringify(ui);
+
+      return page;
+    }
+
+    async function submitDynamicForm(edgeName, pluginName, formId, btn) {
+      const page = btn.closest('.page');
+      const ui = JSON.parse(page.dataset.uiConfig || '{}');
+      const form = (ui.forms || []).find(f => f.id === formId);
+      if (!form) {
+        flash(`Form ${formId} not found`, false);
+        return;
+      }
+
+      // Collect field values
+      const rawData = {};
+      (form.fields || []).forEach(field => {
+        const cssClass = field.css_class || `field-${field.name}`;
+        const el = page.querySelector(`.${cssClass}`);
+        if (el) {
+          if (field.type === 'checkbox') {
+            rawData[field.name] = el.checked;
+          } else {
+            rawData[field.name] = el.value;
+          }
+        }
+      });
+
+      // Transform data based on plugin type
+      let data;
+      if (pluginName === 'psij') {
+        // PsiJ expects: {job_spec: {executable, arguments, attributes}, executor}
+        const args = rawData.args ? rawData.args.trim().split(/\s+/) : [];
+        data = {
+          job_spec: {
+            executable: rawData.exec,
+            arguments: args,
+            attributes: {}
+          },
+          executor: rawData.executor || 'local'
+        };
+        if (rawData.queue) data.job_spec.attributes.queue_name = rawData.queue;
+        if (rawData.account) data.job_spec.attributes.project_name = rawData.account;
+        if (rawData.duration) data.job_spec.attributes.duration = rawData.duration;
+        if (rawData.node_count) data.job_spec.attributes.node_count = parseInt(rawData.node_count, 10);
+      } else if (pluginName === 'rhapsody') {
+        // Rhapsody expects: {tasks: [{executable, arguments, ...}]}
+        const args = rawData.args ? rawData.args.trim().split(/\s+/) : [];
+        data = {
+          tasks: [{
+            executable: rawData.exec,
+            arguments: args,
+            backend: rawData.backends || 'concurrent'
+          }]
+        };
+      } else {
+        // Generic: send raw field values
+        data = rawData;
+      }
+
+      // Determine endpoint
+      const endpoint = form.submit?.endpoint || `submit/{sid}`;
+
+      try {
+        const sid = await registerSession(edgeName, pluginName);
+        const ns = getNs(edgeName, pluginName);
+        const url = `${ns}/${endpoint.replace('{sid}', sid)}`;
+
+        const result = await apiFetch(url, {
+          method: 'POST',
+          body: JSON.stringify(data)
+        });
+
+        flash(`Submitted successfully`);
+
+        // Handle task_list monitor updates
+        handleDynamicSubmitResult(page, pluginName, edgeName, ui, form, data, result);
+
+        // Update args for next submission (only PsiJ launches child edges)
+        if (pluginName === 'psij') {
+          updateEdgeLaunchArgs(page, edgeName, '.p-args');
+        }
+
+      } catch (e) {
+        flash(`Submit error: ${e.message}`, false);
+      }
+    }
+
+    function handleDynamicSubmitResult(page, pluginName, edgeName, ui, form, data, result) {
+      const monitor = (ui.monitors || []).find(m => m.type === 'task_list');
+      if (!monitor) return;
+
+      const output = page.querySelector(`.${monitor.css_class}`);
+      if (!output) return;
+
+      // Clear placeholder text
+      if (output.textContent === monitor.empty_text) {
+        output.innerHTML = '';
+      }
+
+      // Extract task IDs from result
+      const notifications = ui.notifications || {};
+      const idField = notifications.id_field || 'uid';
+      const stateField = notifications.state_field || 'state';
+
+      const results = Array.isArray(result) ? result : [result];
+      results.forEach(r => {
+        const taskId = r[idField];
+        if (!taskId) return;
+
+        // Build label from transformed data
+        let label;
+        if (pluginName === 'psij' && data.job_spec) {
+          const args = (data.job_spec.arguments || []).join(' ');
+          label = `${data.job_spec.executable || ''} ${args}`.trim();
+        } else if (pluginName === 'rhapsody' && data.tasks?.[0]) {
+          const t = data.tasks[0];
+          const args = (t.arguments || []).join(' ');
+          label = `${t.executable || ''} ${args}`.trim();
+        } else {
+          label = data.exec ? `${data.exec} ${data.args || ''}` : JSON.stringify(data).slice(0, 50);
+        }
+        registerTask(edgeName, pluginName, taskId, label);
+
+        // Create task entry - include edge name to avoid collisions between edges
+        const prefix = pluginName === 'psij' ? 'psij-job' : pluginName === 'rhapsody' ? 'rh-task' : `${pluginName}-task`;
+        const entryId = `${prefix}-${edgeName}-${taskId}`;
+        const icon = ui.icon || '🔌';
+        const state = r[stateField] || 'SUBMITTED';
+
+        const taskEntry = document.createElement('details');
+        taskEntry.className = 'task-entry';
+        taskEntry.id = entryId;
+        taskEntry.innerHTML = `
+          <summary>
+            <span class="task-summary-content">
+              <strong style="margin-right:6px;">${icon} ${taskId.slice(0, 12)}…</strong>
+              <span class="${prefix}-state badge badge-orange" style="font-size:.65rem;padding:1px 4px;">${state}</span>
+              <code style="font-size:0.8em;color:var(--muted);margin-left:8px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escHtml(label)}</code>
+            </span>
+          </summary>
+          <div class="task-log ${prefix}-log"><div style="color:var(--muted);font-style:italic;">Waiting…</div></div>`;
+        output.appendChild(taskEntry);
+        output.scrollTop = output.scrollHeight;
+
+        // Check for pending notifications
+        processPendingNotification(edgeName, pluginName, taskId);
+      });
+    }
+
+    async function refreshDynamicPage(btn) {
+      const page = btn.closest('.page');
+      const edgeName = page.dataset.edgeName;
+      const pluginName = page.dataset.pluginName;
+      const ui = JSON.parse(page.dataset.uiConfig || '{}');
+
+      // Find monitors with auto_load
+      for (const monitor of (ui.monitors || [])) {
+        if (monitor.auto_load) {
+          const content = page.querySelector(`.${monitor.css_class}`);
+          if (content) {
+            content.innerHTML = '<div class="empty"><div class="spinner"></div><p style="margin-top:10px">Loading…</p></div>';
+            try {
+              const sid = await registerSession(edgeName, pluginName);
+              const ns = getNs(edgeName, pluginName);
+              const endpoint = monitor.auto_load.replace('{sid}', sid);
+              const data = await apiFetch(`${ns}/${endpoint}`);
+
+              // Use plugin-specific renderer if available, else show JSON
+              if (pluginName === 'sysinfo') {
+                content.innerHTML = renderSysinfo(data);
+              } else if (pluginName === 'queue_info') {
+                content.innerHTML = renderQueueInfo(data, edgeName, sid, ns);
+              } else {
+                content.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
+              }
+            } catch (e) {
+              content.innerHTML = `<div class="card"><p style="color:var(--danger)">Error: ${e.message}</p></div>`;
+            }
+          }
+        }
+      }
     }
 
     // ─────────────────────────────────────────────────────────────
@@ -1270,13 +1597,21 @@
       return p > 80 ? 'warn' : '';
     }
 
+    const API_TIMEOUT_MS = 30000; // 30 second request timeout
+
     async function apiFetch(path, opts = {}) {
       const url = BRIDGE.replace(/\/$/, '') + path;
+      const controller = new AbortController();
+      const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
+
       try {
         const res = await fetch(url, {
           ...opts,
+          signal: controller.signal,
           headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) }
         });
+        clearTimeout(timeoutId);
+
         if (!res.ok) {
           const t = await res.text();
           const errMsg = `HTTP ${res.status}: ${t}`;
@@ -1294,6 +1629,12 @@
         }
         return res.json();
       } catch (e) {
+        clearTimeout(timeoutId);
+        // Timeout errors
+        if (e.name === 'AbortError') {
+          showToast('Request Timeout', `${path}: Request timed out after ${API_TIMEOUT_MS/1000}s`, 'error');
+          throw new Error('Request timed out');
+        }
         // Network errors (connection refused, timeout, etc.)
         if (e.name === 'TypeError' || e.message.includes('Failed to fetch')) {
           showToast('Network Error', `Failed to reach ${path}. Check connection.`, 'error');
@@ -1320,28 +1661,10 @@
       return data.sid;
     }
 
-    function getSid(edgeName, pluginName) {
-      return sessions[`${edgeName}/${pluginName}`];
-    }
-
     function getNs(edgeName, pluginName) {
       return edgeData[edgeName]?.plugins?.[pluginName]?.namespace;
     }
 
-    async function pluginGet(edgeName, pluginName, subPath) {
-      let sid = getSid(edgeName, pluginName);
-      if (!sid) sid = await registerSession(edgeName, pluginName);
-      const ns = getNs(edgeName, pluginName);
-      return apiFetch(`${ns}/${subPath}`);
-    }
-
-    async function pluginPost(edgeName, pluginName, subPath, body = {}) {
-      let sid = getSid(edgeName, pluginName);
-      if (!sid) sid = await registerSession(edgeName, pluginName);
-      const ns = getNs(edgeName, pluginName);
-      return apiFetch(`${ns}/${subPath}`, { method: 'POST', body: JSON.stringify(body) });
-    }
-
     // ─────────────────────────────────────────────────────────────
     //  Navigation
     // ─────────────────────────────────────────────────────────────
@@ -1355,8 +1678,18 @@
     }
 
     // ─────────────────────────────────────────────────────────────
-    //  Connect
+    //  Connect / Disconnect
     // ─────────────────────────────────────────────────────────────
+    let isConnected = false;
+
+    function toggleConnection() {
+      if (isConnected) {
+        doDisconnect();
+      } else {
+        doConnect();
+      }
+    }
+
     async function doConnect() {
       const rawUrl = document.getElementById('bridgeUrl').value.trim();
       if (!rawUrl) { flash('Please enter a Bridge URL', false); return; }
@@ -1380,7 +1713,20 @@
         await loadHierarchy();
         setConnectionStatus('connected');
         flash('Connected to bridge');
-        showPage('hierarchy');
+
+        // Update UI for connected state
+        isConnected = true;
+        document.getElementById('connectBtn').textContent = 'Disconnect';
+        document.getElementById('connectBtn').classList.remove('btn-primary');
+        document.getElementById('connectBtn').classList.add('btn-danger');
+
+        // Update sidebar bridge label with hostname
+        const hostname = extractHostname(BRIDGE);
+        document.getElementById('bridgeLabel').textContent = `Bridge [${hostname}]`;
+        document.getElementById('bridgePageTitle').textContent = `Bridge [${hostname}]`;
+        document.getElementById('nav-bridge').style.display = '';
+
+        showPage('bridge');
 
         // Open SSE event stream for push notifications
         setupEventSource();
@@ -1410,7 +1756,7 @@
           : '';
 
         flash('Connection failed: ' + msg, false);
-        document.getElementById('hierarchyContent').innerHTML =
+        document.getElementById('bridgeInfoContent').innerHTML =
           `<div class="card">
              <p style="color:var(--danger)">Error: ${msg}</p>
              ${warnP}
@@ -1420,6 +1766,56 @@
       }
     }
 
+    function doDisconnect() {
+      // Close SSE connection
+      if (eventSource) {
+        eventSource.close();
+        eventSource = null;
+      }
+
+      // Reset state
+      isConnected = false;
+      BRIDGE = '';
+      edgeData = {};
+      window.lastEdges = {};
+      window.lastBridge = {};
+
+      // Clear task registry
+      for (const key in taskRegistry) delete taskRegistry[key];
+
+      // Clear dynamic pages
+      for (const pid in activeDynamicPages) {
+        const page = activeDynamicPages[pid];
+        if (page && page.parentNode) page.parentNode.removeChild(page);
+        delete activeDynamicPages[pid];
+      }
+
+      // Reset UI
+      document.getElementById('connectBtn').textContent = 'Connect';
+      document.getElementById('connectBtn').classList.remove('btn-danger');
+      document.getElementById('connectBtn').classList.add('btn-primary');
+      document.getElementById('nav-bridge').style.display = 'none';
+      document.getElementById('edgeTree').innerHTML = '';
+      document.getElementById('bridgeInfoContent').innerHTML = `
+        <div class="empty">
+          <div class="empty-icon">🔌</div>
+          <p>Not connected. Enter a Bridge URL and click Connect.</p>
+        </div>`;
+
+      setConnectionStatus('disconnected');
+      flash('Disconnected from bridge');
+      showPage('welcome');
+    }
+
+    function extractHostname(url) {
+      try {
+        const u = new URL(url);
+        return u.hostname + (u.port ? ':' + u.port : '');
+      } catch (e) {
+        return url;
+      }
+    }
+
     function setupEventSource() {
       if (eventSource) eventSource.close();
 
@@ -1541,49 +1937,63 @@
       function renderEdgeSidebar(ename, depth = 0) {
         const edata = edges[ename];
         const plugins = edata.plugins || {};
+        const pluginNames = Object.keys(plugins);
         const indent = depth * 12;
-        let html = `<details id="sidebar-edge-${ename}">
+        let html = `<details id="sidebar-edge-${ename}" open>
           <summary style="display: list-item; list-style-position: inside; padding: 6px 16px; padding-left: ${16 + indent}px; user-select:none; outline:none; font-weight:bold; cursor:pointer; font-size: 0.85rem; color: var(--accent2); transition: background .15s;" onmouseover="this.style.background='var(--bg3)'" onmouseout="this.style.background='transparent'">
-            <span onclick="showPage('hierarchy'); event.stopPropagation();" style="display:inline-flex; align-items:center; gap:8px;">
+            <span onclick="showPage('bridge'); event.stopPropagation();" style="display:inline-flex; align-items:center; gap:8px;">
               <span class="icon">💻</span> ${ename}
             </span>
           </summary>
-          <ul class="tree" style="margin-top: 2px; padding-left: 16px; border-left: 1px dashed var(--border); margin-left: ${24 + indent}px;">`;
+          <ul class="tree" style="margin-top: 2px; padding-left: 8px; border-left: 1px dashed var(--border); margin-left: ${16 + indent}px;">`;
 
-        // Render plugins
-        for (const pname of Object.keys(plugins)) {
-          const pid = `${ename}__${pname}`;
-          const icon = pluginIcon(pname);
-
-          // Add registered tasks under this plugin
-          const tasks = (taskRegistry[ename] && taskRegistry[ename][pname]) || {};
-          const taskKeys = Object.keys(tasks);
-
-          if (taskKeys.length === 0) {
-            html += `<li class="tree-item plugin" id="nav-${pid}"
-                     onclick="showPluginPage('${ename}','${pname}')">
-                   <span class="icon">${icon}</span> ${pname}
-                 </li>`;
-          } else {
-            html += `<details id="sidebar-plugin-${pid}">
-              <summary style="display: list-item; list-style-position: inside; cursor:pointer;" class="tree-item plugin">
-                 <span onclick="showPluginPage('${ename}','${pname}'); event.stopPropagation();" style="display:inline-flex; align-items:center;"><span class="icon">${icon}</span> ${pname}</span>
-              </summary>
-              <ul class="tree" style="margin-top:2px; padding-left:16px; border-left:1px dashed var(--border); margin-left:16px;">`;
-            for (const [tid, tinfo] of Object.entries(tasks)) {
-              const short = tid.length > 10 ? tid.slice(0, 8) + '…' : tid;
-              const badge = taskStateBadge(tinfo.state);
-              const domId = pname === 'psij' ? `psij-job-${tid}` : `rh-task-${tid}`;
-              html += `<li class="tree-item" style="padding-left:12px; font-size:0.8rem; opacity:0.9;"
-                       onclick="showTaskEntry('${ename}','${pname}','${domId}')">
-                     ${badge} <code style="font-size:0.75rem;">${short}</code>
+        // Render plugins under a collapsible "plugins" entry (collapsed by default)
+        if (pluginNames.length > 0) {
+          html += `<li style="list-style:none;"><details id="sidebar-plugins-${ename}">
+            <summary style="display: list-item; list-style-position: inside; cursor:pointer; font-size:0.8rem; color:var(--muted); padding:2px 0;">
+              <span style="display:inline-flex; align-items:center; gap:6px;">
+                <span class="icon">🔌</span> plugins (${pluginNames.length})
+              </span>
+            </summary>
+            <ul class="tree" style="margin-top:2px; padding-left:8px; border-left:1px dashed var(--border); margin-left:8px;">`;
+
+          for (const pname of pluginNames) {
+            const pid = `${ename}__${pname}`;
+            const icon = pluginIcon(pname, ename);
+
+            // Add registered tasks under this plugin
+            const tasks = (taskRegistry[ename] && taskRegistry[ename][pname]) || {};
+            const taskKeys = Object.keys(tasks);
+
+            if (taskKeys.length === 0) {
+              html += `<li class="tree-item plugin" id="nav-${pid}"
+                       onclick="showPluginPage('${ename}','${pname}')">
+                     <span class="icon">${icon}</span> ${pname}
                    </li>`;
+            } else {
+              html += `<details id="sidebar-plugin-${pid}">
+                <summary style="display: list-item; list-style-position: inside; cursor:pointer;" class="tree-item plugin"
+                         onclick="showPluginPage('${ename}','${pname}')">
+                   <span style="display:inline-flex; align-items:center;"><span class="icon">${icon}</span> ${pname}</span>
+                </summary>
+                <ul class="tree" style="margin-top:2px; padding-left:8px; border-left:1px dashed var(--border); margin-left:8px;">`;
+              for (const [tid, tinfo] of Object.entries(tasks)) {
+                const short = tid.length > 10 ? tid.slice(0, 8) + '…' : tid;
+                const badge = taskStateBadge(tinfo.state);
+                const domId = pname === 'psij' ? `psij-job-${ename}-${tid}` : `rh-task-${ename}-${tid}`;
+                html += `<li class="tree-item" style="padding-left:12px; font-size:0.8rem; opacity:0.9;"
+                         onclick="showTaskEntry('${ename}','${pname}','${domId}')">
+                       ${badge} <code style="font-size:0.75rem;">${short}</code>
+                     </li>`;
+              }
+              html += `</ul></details>`;
             }
-            html += `</ul></details>`;
           }
+
+          html += `</ul></details></li>`;
         }
 
-        // Render child edges (nested under this edge)
+        // Render child edges (at same level as plugins entry)
         if (children[ename]) {
           for (const cname of children[ename]) {
             html += `<li style="list-style:none; margin-left:-8px;">`;
@@ -1608,21 +2018,29 @@
       setTimeout(() => {
         const el = document.getElementById(domId);
         if (el) {
-          el.open = true;
-          el.scrollIntoView({ behavior: 'smooth', block: 'center' });
-          el.style.outline = '2px solid var(--accent)';
-          setTimeout(() => el.style.outline = '', 1500);
+          // Toggle open/close
+          el.open = !el.open;
+          // Brief highlight without scrolling (keep left hierarchy stable)
+          if (el.open) {
+            el.style.outline = '2px solid var(--accent)';
+            setTimeout(() => el.style.outline = '', 1500);
+          }
         }
       }, 100);
     }
 
-    function pluginIcon(p) {
-      return { sysinfo: '🖥️', queue_info: '📋', psij: '🚀', rhapsody: '🎼', lucid: '🧠' }[p] || '🔌';
+    function pluginIcon(p, edgeName) {
+      // Try to get icon from ui_config if edgeName is provided
+      if (edgeName) {
+        const ui = getPluginUIConfig(edgeName, p);
+        if (ui?.icon) return ui.icon;
+      }
+      // Fallback to hardcoded icons
+      return { sysinfo: '🖥️', queue_info: '📋', psij: '🚀', rhapsody: '🎼', lucid: '🧠', xgfabric: '🕸️' }[p] || '🔌';
     }
 
     function renderHierarchy(edges, bridge) {
       let html = `<div class="card">
-        <div class="card-title" style="margin-bottom:15px">🗂️ Endpoint Hierarchy</div>
         <div style="font-size: 0.95em; line-height: 1.8;">`;
 
       // Bridge level
@@ -1682,7 +2100,7 @@
             // Render plugins
             for (const pname of plugins) {
               const pinfo = edata.plugins[pname];
-              const pIcon = pluginIcon(pname);
+              const pIcon = pluginIcon(pname, ename);
               ehtml += `<div style="display:flex; align-items:center; cursor:pointer; padding: 2px 8px; border-radius:4px; transition: background 0.2s; margin-top:2px; margin-left:-8px;" 
                             onmouseover="this.style.background='var(--bg3)'" 
                             onmouseout="this.style.background='transparent'"
@@ -1701,7 +2119,7 @@
                 for (const tid of tids) {
                   const tinfo = tasks[tid];
                   const badge = taskStateBadge(tinfo.state);
-                  const domId = pname === 'psij' ? `psij-job-${tid}` : `rh-task-${tid}`;
+                  const domId = pname === 'psij' ? `psij-job-${ename}-${tid}` : `rh-task-${ename}-${tid}`;
                   ehtml += `<div style="font-size:0.8em; padding:1px 0; cursor:pointer; display:flex; align-items:center; gap:6px; opacity:0.85" onclick="showTaskEntry('${ename}','${pname}','${domId}')">
                     ${badge} <code style="font-size:0.75rem">${tid.slice(0, 8)}…</code> <span style="color:var(--muted); font-size:0.8em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:200px;">${tinfo.label || ''}</span>
                   </div>`;
@@ -1730,7 +2148,7 @@
       }
 
       html += `</div></div></div>`;
-      document.getElementById('hierarchyContent').innerHTML = html;
+      document.getElementById('bridgeInfoContent').innerHTML = html;
     }
 
     // ─────────────────────────────────────────────────────────────
@@ -1743,20 +2161,41 @@
 
       // Create page if not exists
       if (!activeDynamicPages[pid]) {
-        const templateId = `page-${pluginName}-template`;
-        const tmpl = document.getElementById(templateId);
-        if (!tmpl) {
-          flash(`No UI template for plugin: ${pluginName}`, false);
-          return;
+        let page = null;
+
+        // Try dynamic rendering from ui_config first
+        const ui = getPluginUIConfig(edgeName, pluginName);
+        if (ui && ui.title) {
+          page = createDynamicPluginPage(edgeName, pluginName);
+          if (page) {
+            page.id = 'page-' + pid;
+            document.querySelector('main').appendChild(page);
+          }
+        }
+
+        // Fall back to hardcoded template
+        if (!page) {
+          const templateId = `page-${pluginName}-template`;
+          const tmpl = document.getElementById(templateId);
+          if (!tmpl) {
+            flash(`No UI template for plugin: ${pluginName}`, false);
+            return;
+          }
+          page = tmpl.cloneNode(true);
+          page.id = 'page-' + pid;
+          page.style.display = '';
+          page.querySelector('.edge-label').textContent = edgeName;
+          page.dataset.edgeName = edgeName;
+          page.dataset.pluginName = pluginName;
+          document.querySelector('main').appendChild(page);
+        }
+
+        activeDynamicPages[pid] = page;
+
+        // Pre-fill PsiJ defaults on first page creation (only PsiJ launches child edges)
+        if (pluginName === 'psij') {
+          prefillEdgeLaunchArgs(page, edgeName, '.p-args');
         }
-        const clone = tmpl.cloneNode(true);
-        clone.id = 'page-' + pid;
-        clone.style.display = '';
-        clone.querySelector('.edge-label').textContent = edgeName;
-        clone.dataset.edgeName = edgeName;
-        clone.dataset.pluginName = pluginName;
-        document.querySelector('main').appendChild(clone);
-        activeDynamicPages[pid] = clone;
       }
 
       showPage(pid);
@@ -1764,61 +2203,64 @@
       const navEl = document.getElementById(`nav-${pid}`);
       if (navEl) navEl.classList.add('active');
 
-      // Auto-load data
+      // Auto-load data for monitors with auto_load
       const page = activeDynamicPages[pid];
-      if (pluginName === 'sysinfo') autoLoadSysinfo(page);
-      if (pluginName === 'queue_info') autoLoadQueueInfo(page);
-
-      // Pre-fill PsiJ defaults for launching a child edge
-      if (pluginName === 'psij') {
-        const argsInput = page.querySelector('.p-args');
-        if (argsInput && !argsInput.value) {
-          const url = BRIDGE.replace(/\/$/, '');
-          const childName = getNextEdgeChildName(edgeName);
-          argsInput.value = `--url ${url} --name ${childName}`;
-        }
-        
-        // Populate Queue/Account selectors from cache if available
-        const qCache = queueDataCache[edgeName];
-        if (qCache) {
-          const queueInput = page.querySelector('.p-queue');
-          if (queueInput && queueInput.tagName === 'INPUT') {
-            const select = document.createElement('select');
-            select.className = 'p-queue';
-            
-            const qList = Object.values(qCache.queues || {});
-            const getQName = q => q.name || q.partition || q;
-            
-            // Priority selection logic: debug > interactive > first available
-            const pb = qList.find(q => getQName(q) === 'debug');
-            const pi = qList.find(q => getQName(q) === 'interactive');
-            const defaultQ = (pb || pi || qList[0]);
-            const defaultQName = defaultQ ? getQName(defaultQ) : "";
-            
-            select.innerHTML = '<option value="">(none)</option>' + qList.map(q => {
-              const qn = getQName(q);
-              return `<option value="${qn}" ${qn === defaultQName ? 'selected' : ''}>${qn}</option>`;
-            }).join('');
-            queueInput.parentNode.replaceChild(select, queueInput);
-          }
-          
-          const accountInput = page.querySelector('.p-account');
-          if (accountInput && accountInput.tagName === 'INPUT') {
-            const select = document.createElement('select');
-            select.className = 'p-account';
-            
-            // Extract unique accounts
-            const accountSet = new Set();
-            (qCache.allocations || []).forEach(a => {
-              if (a.account) accountSet.add(a.account);
-            });
-            const accounts = Array.from(accountSet);
-            const defaultAcc = accounts.length > 0 ? accounts[0] : "";
-            
-            select.innerHTML = '<option value="">(none)</option>' + accounts.map(a => `<option value="${a}" ${a === defaultAcc ? 'selected' : ''}>${a}</option>`).join('');
-            accountInput.parentNode.replaceChild(select, accountInput);
+      const ui = page.dataset.uiConfig ? JSON.parse(page.dataset.uiConfig) : null;
+      if (ui) {
+        // Dynamic page - auto-load monitors
+        for (const monitor of (ui.monitors || [])) {
+          if (monitor.auto_load) {
+            autoLoadDynamicMonitor(page, edgeName, pluginName, monitor);
           }
         }
+      } else {
+        // Legacy hardcoded pages
+        if (pluginName === 'sysinfo') autoLoadSysinfo(page);
+        if (pluginName === 'queue_info') autoLoadQueueInfo(page);
+      }
+    }
+
+    function prefillEdgeLaunchArgs(page, edgeName, selector) {
+      const argsInput = page.querySelector(selector);
+      if (argsInput && !argsInput.value) {
+        const url = BRIDGE.replace(/\/$/, '');
+        const childName = getNextEdgeChildName(edgeName);
+        argsInput.value = `--url ${url} --name ${childName}`;
+      }
+    }
+
+    function updateEdgeLaunchArgs(page, edgeName, selector) {
+      const argsInput = page.querySelector(selector);
+      if (argsInput) {
+        const url = BRIDGE.replace(/\/$/, '');
+        const childName = getNextEdgeChildName(edgeName);
+        argsInput.value = `--url ${url} --name ${childName}`;
+      }
+    }
+
+    async function autoLoadDynamicMonitor(page, edgeName, pluginName, monitor) {
+      const content = page.querySelector(`.${monitor.css_class}`);
+      if (!content) return;
+
+      content.innerHTML = '<div class="empty"><div class="spinner"></div><p style="margin-top:10px">Loading…</p></div>';
+
+      try {
+        const sid = await registerSession(edgeName, pluginName);
+        const ns = getNs(edgeName, pluginName);
+        const endpoint = monitor.auto_load.replace('{sid}', sid);
+        const data = await apiFetch(`${ns}/${endpoint}`);
+
+        // Use plugin-specific renderer if available
+        if (pluginName === 'sysinfo') {
+          content.innerHTML = renderSysinfo(data);
+        } else if (pluginName === 'queue_info') {
+          content.innerHTML = renderQueueInfo(data, edgeName, sid, ns);
+        } else {
+          content.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
+        }
+      } catch (e) {
+        content.innerHTML = `<div class="card"><p style="color:var(--danger)">Error: ${e.message}</p></div>`;
+        flash(`${pluginName} error: ${e.message}`, false);
       }
     }
 
@@ -1956,17 +2398,8 @@
       try {
         const sid = await registerSession(edgeName, 'queue_info');
         const ns = getNs(edgeName, 'queue_info');
-        const [info, allocs] = await Promise.all([
-          apiFetch(`${ns}/get_info/${sid}`),
-          apiFetch(`${ns}/list_allocations/${sid}`)
-        ]);
-        
-        const queuesObj = info.partitions || info.queues || info || {};
-        const queues = Array.isArray(queuesObj) ? queuesObj : Object.values(queuesObj);
-        const allocations = allocs.allocations || [];
-        queueDataCache[edgeName] = { queues, allocations };
-        
-        content.innerHTML = renderQueueInfo(queues, allocations, edgeName, sid, ns);
+        const info = await apiFetch(`${ns}/get_info/${sid}`);
+        content.innerHTML = renderQueueInfo(info, edgeName, sid, ns);
       } catch (e) {
         content.innerHTML = `<div class="card"><p style="color:var(--danger)">Error: ${e.message}</p></div>`;
         flash('QueueInfo error: ' + e.message, false);
@@ -1978,50 +2411,32 @@
       autoLoadQueueInfo(page);
     }
 
-    function renderQueueInfo(partitions, allocations, edgeName, sid, ns) {
-      let html = "";
-      
+    function renderQueueInfo(info, edgeName, sid, ns) {
+      const partitions = info.partitions || info.queues || info || [];
       if (!Array.isArray(partitions) || !partitions.length) {
-        html += `<div class="card"><p style="color:var(--muted)">No partition data returned.</p></div>`;
-      } else {
-        html += `<div class="card"><div class="card-title">📋 Partitions / Queues</div>
-      <table><thead><tr><th>Name</th><th>State</th><th>Nodes</th><th>CPUs</th><th>Jobs</th><th>Actions</th></tr></thead><tbody>`;
-
-        for (const p of partitions) {
-          const name = p.name || p.partition || JSON.stringify(p).slice(0, 30);
-          const state = p.state || p.avail || '-';
-          const stateBadge = state.toLowerCase().includes('up') ? 'badge-green' : 'badge-orange';
-          html += `<tr>
-        <td><strong>${name}</strong></td>
-        <td><span class="badge ${stateBadge}">${state}</span></td>
-        <td>${p.nodes || p.total_nodes || '-'}</td>
-        <td>${p.cpus || p.total_cpus || '-'}</td>
-        <td>${p.jobs || '-'}</td>
-        <td><button class="btn btn-secondary btn-sm"
-            onclick="loadQueueJobs('${edgeName}','${sid}','${ns}','${name}',this)">
-            View Jobs</button></td>
-      </tr>`;
-        }
-        html += '</tbody></table></div>';
+        return `<div class="card"><p style="color:var(--muted)">No partition data returned. Raw response:</p>
+      <div class="output-box" style="margin-top:10px">${JSON.stringify(info, null, 2)}</div></div>`;
       }
-      
-      if (Array.isArray(allocations) && allocations.length > 0) {
-        html += `<div class="card"><div class="card-title">💼 Allocations / Projects</div>
-      <table><thead><tr><th>Account</th><th>User</th><th>Fairshare</th><th>QoS</th><th>Max Jobs</th></tr></thead><tbody>`;
 
-        for (const a of allocations) {
-          html += `<tr>
-        <td><strong>${a.account || '-'}</strong></td>
-        <td>${a.user || '-'}</td>
-        <td>${a.fairshare || '-'}</td>
-        <td><span class="badge badge-gray">${a.qos || '-'}</span></td>
-        <td>${a.max_jobs || '-'}</td>
-      </tr>`;
-        }
-        html += '</tbody></table></div>';
-      }
+      let html = `<div class="card"><div class="card-title">📋 Partitions / Queues</div>
+    <table><thead><tr><th>Name</th><th>State</th><th>Nodes</th><th>CPUs</th><th>Jobs</th><th>Actions</th></tr></thead><tbody>`;
 
-      html += '<div id="qi-jobs-area"></div>';
+      for (const p of partitions) {
+        const name = p.name || p.partition || JSON.stringify(p).slice(0, 30);
+        const state = p.state || p.avail || '-';
+        const stateBadge = state.toLowerCase().includes('up') ? 'badge-green' : 'badge-orange';
+        html += `<tr>
+      <td><strong>${name}</strong></td>
+      <td><span class="badge ${stateBadge}">${state}</span></td>
+      <td>${p.nodes || p.total_nodes || '-'}</td>
+      <td>${p.cpus || p.total_cpus || '-'}</td>
+      <td>${p.jobs || '-'}</td>
+      <td><button class="btn btn-secondary btn-sm"
+          onclick="loadQueueJobs('${edgeName}','${sid}','${ns}','${name}',this)">
+          View Jobs</button></td>
+    </tr>`;
+      }
+      html += '</tbody></table></div><div id="qi-jobs-area"></div>';
       return html;
     }
 
@@ -2058,73 +2473,63 @@
     // ─────────────────────────────────────────────────────────────
     //  SSE Notification handler
     // ─────────────────────────────────────────────────────────────
+
+    // Plugin notification config: maps plugin name to DOM element prefixes and data fields
+    const NOTIFICATION_CONFIG = {
+      psij: {
+        prefix: 'psij-job',
+        stateClass: 'psij-job-state',
+        logClass: 'psij-job-log',
+        topic: 'job_status',
+        idField: 'job_id'
+      },
+      rhapsody: {
+        prefix: 'rh-task',
+        stateClass: 'rh-task-state',
+        logClass: 'rh-task-log',
+        topic: 'task_status',
+        idField: 'uid'
+      }
+    };
+
     function handleNotification(data) {
-      console.log('[handleNotification] Received:', JSON.stringify(data));
       const plugin = data.plugin;
       const topic = data.topic;
       const d = data.data || {};
+      const edgeName = data.edge || '';
 
-      if (plugin === 'psij' && topic === 'job_status') {
-        const jobId = d.job_id;
-        const state = d.state || '?';
-        updateTaskState(data.edge || '', 'psij', jobId, state);
-
-        // Try to update the DOM element
-        if (!applyPsijNotification(jobId, state, d)) {
-          // Element doesn't exist yet - queue for later (race condition)
-          console.log(`[psij] Queuing notification for ${jobId} (element not ready)`);
-          pendingNotifications[`psij-${jobId}`] = { data: d, state, timestamp: Date.now() };
-        }
-
-      } else if (plugin === 'rhapsody' && topic === 'task_status') {
-        const uid = d.uid || '';
-        const state = d.state || '?';
-        console.log(`[rhapsody] Notification for task ${uid} -> ${state} (edge: ${data.edge})`);
-
-        updateTaskState(data.edge || '', 'rhapsody', uid, state);
-
-        // Try to update the DOM element
-        if (!applyRhapsodyNotification(uid, state, d)) {
-          // Element doesn't exist yet - queue for later (race condition)
-          console.log(`[rhapsody] Queuing notification for ${uid} (element not ready)`);
-          pendingNotifications[uid] = { data: d, state, timestamp: Date.now() };
-        }
-
-        const ts = new Date().toLocaleTimeString();
-        const isOk = ['DONE', 'COMPLETED'].some(s => (state || '').toUpperCase().includes(s));
-        stateEl.className = `rh-task-state badge ${isOk ? 'badge-green' : 'badge-red'}`;
-        stateEl.textContent = state;
-        entry.open = true;
-
-        const rc = d.exit_code ?? d.retval ?? '?';
-        let logHtml = `<span style="color:var(--muted);font-size:0.9em;">[${ts}] <b>${state}</b> rc:${rc}</span>`;
-        if (d.stdout) logHtml += `<pre class="ok">out: ${escHtml(d.stdout).trim()}</pre>`;
-        if (d.stderr) logHtml += `<pre class="err">err: ${escHtml(d.stderr).trim()}</pre>`;
-        if (d.exception) logHtml += `<pre class="err">exc: ${escHtml(d.exception).trim()}</pre>`;
-        logEl.innerHTML = logHtml;
+      const config = NOTIFICATION_CONFIG[plugin];
+      if (!config || config.topic !== topic) return;
+
+      const taskId = d[config.idField] || '';
+      const state = d.state || '?';
+
+      updateTaskState(edgeName, plugin, taskId, state);
+      if (!applyTaskNotification(plugin, edgeName, taskId, state, d)) {
+        const pendingKey = `${config.prefix}-${edgeName}-${taskId}`;
+        pendingNotifications[pendingKey] = { edgeName, data: d, state, timestamp: Date.now() };
       }
     }
 
-    /**
-     * Apply a rhapsody task notification to the DOM.
-     * Returns true if successful, false if element not found.
-     */
-    function applyRhapsodyNotification(uid, state, d) {
-      const entryId = `rh-task-${uid}`;
+    function applyTaskNotification(plugin, edgeName, taskId, state, d) {
+      const config = NOTIFICATION_CONFIG[plugin];
+      if (!config) return false;
+
+      const entryId = `${config.prefix}-${edgeName}-${taskId}`;
       const entry = document.getElementById(entryId);
       if (!entry) return false;
 
-      const stateEl = entry.querySelector('.rh-task-state');
-      const logEl = entry.querySelector('.rh-task-log');
+      const stateEl = entry.querySelector(`.${config.stateClass}`);
+      const logEl = entry.querySelector(`.${config.logClass}`);
       if (!stateEl || !logEl) return false;
 
       const ts = new Date().toLocaleTimeString();
-      const isOk = ['DONE', 'COMPLETED'].some(s => (state || '').toUpperCase().includes(s));
-      const isFailed = ['FAILED', 'ERROR', 'CANCELED'].some(s => (state || '').toUpperCase().includes(s));
+      const stateUpper = (state || '').toUpperCase();
+      const isOk = ['DONE', 'COMPLETED'].some(s => stateUpper.includes(s));
+      const isFailed = ['FAILED', 'ERROR', 'CANCELED'].some(s => stateUpper.includes(s));
 
-      stateEl.className = `rh-task-state badge ${isOk ? 'badge-green' : isFailed ? 'badge-red' : 'badge-orange'}`;
+      stateEl.className = `${config.stateClass} badge ${isOk ? 'badge-green' : isFailed ? 'badge-red' : 'badge-orange'}`;
       stateEl.textContent = state;
-      entry.open = true;
 
       const rc = d.exit_code ?? d.retval ?? '?';
       let logHtml = `<span style="color:var(--muted);font-size:0.9em;">[${ts}] <b>${state}</b> rc:${rc}</span>`;
@@ -2132,59 +2537,21 @@
       if (d.stderr) logHtml += `<pre class="err">err: ${escHtml(d.stderr).trim()}</pre>`;
       if (d.exception) logHtml += `<pre class="err">exc: ${escHtml(d.exception).trim()}</pre>`;
       logEl.innerHTML = logHtml;
-
-      console.log(`[rhapsody] Updated DOM for ${uid}: ${state}`);
       return true;
     }
 
-    /**
-     * Check for pending notifications after creating a task element.
-     */
-    function processPendingNotification(uid, plugin = 'rhapsody') {
-      const key = plugin === 'psij' ? `psij-${uid}` : uid;
+    function processPendingNotification(edgeName, plugin, taskId) {
+      const config = NOTIFICATION_CONFIG[plugin];
+      if (!config) return;
+
+      const key = `${config.prefix}-${edgeName}-${taskId}`;
       const pending = pendingNotifications[key];
       if (pending) {
-        console.log(`[${plugin}] Processing queued notification for ${uid}`);
-        if (plugin === 'psij') {
-          applyPsijNotification(uid, pending.state, pending.data);
-        } else {
-          applyRhapsodyNotification(uid, pending.state, pending.data);
-        }
+        applyTaskNotification(plugin, edgeName, taskId, pending.state, pending.data);
         delete pendingNotifications[key];
       }
     }
 
-    /**
-     * Apply a PsiJ job notification to the DOM.
-     * Returns true if successful, false if element not found.
-     */
-    function applyPsijNotification(jobId, state, d) {
-      const entryId = `psij-job-${jobId}`;
-      const entry = document.getElementById(entryId);
-      if (!entry) return false;
-
-      const stateEl = entry.querySelector('.psij-job-state');
-      const logEl = entry.querySelector('.psij-job-log');
-      if (!stateEl || !logEl) return false;
-
-      const ts = new Date().toLocaleTimeString();
-      const isOk = (state || '').includes('COMPLETED');
-      const isFailed = ['FAILED', 'CANCELED'].some(s => (state || '').includes(s));
-
-      stateEl.className = `psij-job-state badge ${isOk ? 'badge-green' : isFailed ? 'badge-red' : 'badge-orange'}`;
-      stateEl.textContent = state;
-      entry.open = true;
-
-      const rc = d.exit_code ?? '?';
-      let logHtml = `<span style="color:var(--muted);font-size:0.9em;">[${ts}] <b>${state}</b> rc:${rc}</span>`;
-      if (d.stdout) logHtml += `<pre class="ok">out: ${escHtml(d.stdout).trim()}</pre>`;
-      if (d.stderr) logHtml += `<pre class="err">err: ${escHtml(d.stderr).trim()}</pre>`;
-      logEl.innerHTML = logHtml;
-
-      console.log(`[psij] Updated DOM for ${jobId}: ${state}`);
-      return true;
-    }
-
     // ─────────────────────────────────────────────────────────────
     //  PsiJ
     // ─────────────────────────────────────────────────────────────
@@ -2196,20 +2563,6 @@
       return `${edgeName}.${psijEdgeCounters[edgeName]}`;
     }
 
-    function addPsijAttributeRow(btn) {
-      const container = btn.previousElementSibling;
-      const row = document.createElement('div');
-      row.style.display = 'flex';
-      row.style.gap = '10px';
-      row.style.marginBottom = '8px';
-      row.innerHTML = `
-        <input class="p-attr-key" type="text" placeholder="Key (e.g. slurm.constraint)" style="flex:1;" />
-        <input class="p-attr-val" type="text" placeholder="Value (e.g. cpu_gen_1)" style="flex:2;" />
-        <button class="btn btn-secondary btn-sm" onclick="this.parentElement.remove()" style="padding: 4px 10px;">❌</button>
-      `;
-      container.appendChild(row);
-    }
-
     async function submitPsijJob(btn) {
       const page = btn.closest('.page');
       const edgeName = page.dataset.edgeName;
@@ -2224,18 +2577,9 @@
 
       const job_spec = { executable: exec, arguments: args, attributes: {} };
       if (queue) job_spec.attributes.queue_name = queue;
-      if (account) job_spec.attributes.account = account;
+      if (account) job_spec.attributes.project_name = account;
       if (duration) job_spec.attributes.duration = duration;
 
-      const attrRows = page.querySelectorAll('.psij-attribute-rows > div');
-      attrRows.forEach(row => {
-        const key = row.querySelector('.p-attr-key').value.trim();
-        const val = row.querySelector('.p-attr-val').value.trim();
-        if (key && val) {
-          job_spec.attributes[key] = val;
-        }
-      });
-
       // Clear placeholder
       if (output.textContent === 'No jobs submitted yet.') output.innerHTML = '';
 
@@ -2269,7 +2613,7 @@
         output.scrollTop = output.scrollHeight;
 
         // Check if notification arrived before element was created (race condition)
-        processPendingNotification(jobId, 'psij');
+        processPendingNotification(edgeName, 'psij', jobId);
 
         // Update the args field for the NEXT submission (increment counter)
         const argsInput = page.querySelector('.p-args');
@@ -2314,14 +2658,12 @@
 
         flash(`Rhapsody task(s) submitted: ${uids.join(', ')}`);
 
-        // Create per-task expandable entries — updates arrive via SSE
         for (const uid of uids) {
-          console.log(`[rhapsody-submit] Creating task entry with id=rh-task-${uid} on edge=${edgeName}`);
           registerTask(edgeName, 'rhapsody', uid, `${exec} ${args.join(' ')}`);
 
           const taskEntry = document.createElement('details');
           taskEntry.className = 'task-entry';
-          taskEntry.id = `rh-task-${uid}`;
+          taskEntry.id = `rh-task-${edgeName}-${uid}`;
           taskEntry.innerHTML = `
             <summary>
               <span class="task-summary-content">
@@ -2335,7 +2677,7 @@
           output.scrollTop = output.scrollHeight;
 
           // Check if notification arrived before element was created (race condition)
-          processPendingNotification(uid);
+          processPendingNotification(edgeName, 'rhapsody', uid);
         }
 
       } catch (e) {
@@ -2348,8 +2690,6 @@
       return (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
     }
 
-    function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
-
     // ─────────────────────────────────────────────────────────────
     //  Init
     // ─────────────────────────────────────────────────────────────
@@ -2357,10 +2697,44 @@
       if (e.key === 'Enter') doConnect();
     });
 
-    // Auto-fill form and optionally connect if URL is current host.
+    // Sidebar resize functionality
+    (function() {
+      const sidebar = document.getElementById('sidebar');
+      const handle = document.getElementById('sidebarResizeHandle');
+      let isResizing = false;
+
+      handle.addEventListener('mousedown', e => {
+        isResizing = true;
+        handle.classList.add('dragging');
+        document.body.style.cursor = 'ew-resize';
+        document.body.style.userSelect = 'none';
+        e.preventDefault();
+      });
+
+      document.addEventListener('mousemove', e => {
+        if (!isResizing) return;
+        const newWidth = e.clientX;
+        const minW = parseInt(getComputedStyle(sidebar).minWidth) || 160;
+        const maxW = window.innerWidth * 0.5;
+        if (newWidth >= minW && newWidth <= maxW) {
+          sidebar.style.width = newWidth + 'px';
+        }
+      });
+
+      document.addEventListener('mouseup', () => {
+        if (isResizing) {
+          isResizing = false;
+          handle.classList.remove('dragging');
+          document.body.style.cursor = '';
+          document.body.style.userSelect = '';
+        }
+      });
+    })();
+
+    // Auto-fill form and auto-connect if served from bridge (HTTP/HTTPS).
     if (window.location.protocol.startsWith('http')) {
       document.getElementById('bridgeUrl').value = window.location.origin;
-      // You could optionally auto-connect here: doConnect();
+      doConnect();
     }
   </script>
 </body>
