@@ -72,6 +72,7 @@ function adminNavigateTo(page) {
7272 adminState . currentPage = basePage ;
7373 adminState . serverId = param ? parseInt ( param , 10 ) : null ;
7474 adminState . userId = param ? parseInt ( param , 10 ) : null ;
75+ adminState . nodeId = param ? parseInt ( param , 10 ) : null ;
7576 updateAdminNav ( ) ;
7677
7778 if ( basePage === 'server' && adminState . serverId ) {
@@ -80,6 +81,12 @@ function adminNavigateTo(page) {
8081 } else if ( basePage === 'user' && adminState . userId ) {
8182 renderAdminUserDetail ( adminState . userId ) ;
8283 history . pushState ( { adminPage : 'user' , userId : adminState . userId } , '' , `/admin/user/${ adminState . userId } ` ) ;
84+ } else if ( basePage === 'node' && adminState . nodeId ) {
85+ renderAdminNodeDetail ( adminState . nodeId ) ;
86+ history . pushState ( { adminPage : 'node' , nodeId : adminState . nodeId } , '' , `/admin/node/${ adminState . nodeId } ` ) ;
87+ } else if ( basePage === 'nodes' ) {
88+ renderAdminNodes ( ) ;
89+ history . pushState ( { adminPage : 'nodes' } , '' , '/admin/nodes' ) ;
8390 } else if ( basePage === 'users' ) {
8491 renderAdminUsers ( ) ;
8592 history . pushState ( { adminPage : 'users' } , '' , '/admin/users' ) ;
@@ -212,6 +219,10 @@ function renderAdminLayout() {
212219 <i data-lucide="server" style="width:18px;height:18px"></i>
213220 Servers
214221 </a>
222+ <a class="admin-nav-link" data-page="nodes" href="/admin/nodes">
223+ <i data-lucide="network" style="width:18px;height:18px"></i>
224+ Nodes
225+ </a>
215226 <a class="admin-nav-link" data-page="users" href="/admin/users">
216227 <i data-lucide="users" style="width:18px;height:18px"></i>
217228 Users
@@ -237,6 +248,8 @@ function renderAdminLayout() {
237248 <div class="admin-page" id="admin-page-dashboard"></div>
238249 <div class="admin-page active" id="admin-page-servers"></div>
239250 <div class="admin-page" id="admin-page-server-detail"></div>
251+ <div class="admin-page" id="admin-page-nodes"></div>
252+ <div class="admin-page" id="admin-page-node-detail"></div>
240253 <div class="admin-page" id="admin-page-users"></div>
241254 <div class="admin-page" id="admin-page-user-detail"></div>
242255 <div class="admin-page" id="admin-page-activity"></div>
@@ -286,6 +299,17 @@ function renderAdminLayout() {
286299 adminState . userId = uid ;
287300 $a ( '#admin-page-user-detail' ) . classList . add ( 'active' ) ;
288301 renderAdminUserDetail ( uid ) ;
302+ } else if ( basePage === 'node' && param ) {
303+ const nid = parseInt ( param , 10 ) ;
304+ adminState . currentPage = 'node' ;
305+ updateAdminNav ( ) ;
306+ adminState . nodeId = nid ;
307+ $a ( '#admin-page-node-detail' ) . classList . add ( 'active' ) ;
308+ renderAdminNodeDetail ( nid ) ;
309+ } else if ( basePage === 'nodes' ) {
310+ adminState . currentPage = 'nodes' ;
311+ updateAdminNav ( ) ;
312+ renderAdminNodes ( ) ;
289313 } else if ( basePage === 'users' ) {
290314 adminState . currentPage = 'users' ;
291315 updateAdminNav ( ) ;
@@ -1941,6 +1965,236 @@ function showDeleteNestConfirm(nestId, name) {
19411965 } ) ;
19421966}
19431967
1968+ // ─── Nodes ──────────────────────────────────────────────
1969+ async function renderAdminNodes ( ) {
1970+ document . querySelectorAll ( '.admin-page' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
1971+ const el = $a ( '#admin-page-nodes' ) ;
1972+ if ( ! el ) return ;
1973+ el . classList . add ( 'active' ) ;
1974+ el . innerHTML = ahtml `
1975+ <div class="page-header">
1976+ <h1 class="page-title">Nodes</h1>
1977+ <p class="page-subtitle">Pterodactyl panel nodes</p>
1978+ </div>
1979+ <div class="table-container">
1980+ <table>
1981+ <thead>
1982+ <tr>
1983+ <th>ID</th>
1984+ <th>Name</th>
1985+ <th>FQDN</th>
1986+ <th>RAM</th>
1987+ <th>Disk</th>
1988+ <th>CPU</th>
1989+ <th>Allocations</th>
1990+ <th>Status</th>
1991+ </tr>
1992+ </thead>
1993+ <tbody id="admin-nodes-tbody">
1994+ <tr><td colspan="8" style="text-align:center;padding:32px;color:var(--text-secondary)"><span class="spinner"></span> Loading...</td></tr>
1995+ </tbody>
1996+ </table>
1997+ </div>
1998+ ` ;
1999+
2000+ try {
2001+ const data = await adminApi ( '/nodes' ) ;
2002+ const tbody = $a ( '#admin-nodes-tbody' ) ;
2003+ if ( ! tbody ) return ;
2004+
2005+ if ( data . nodes . length === 0 ) {
2006+ tbody . innerHTML = '<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--text-secondary)">No nodes found in the panel.</td></tr>' ;
2007+ return ;
2008+ }
2009+
2010+ tbody . innerHTML = data . nodes . map ( n => {
2011+ const ramMB = n . memory ? Math . round ( n . memory / 1024 ) : '—' ;
2012+ const diskGB = n . disk ? ( n . disk / 1024 ) . toFixed ( 1 ) : '—' ;
2013+ const cpuPct = n . cpu ? n . cpu + '%' : '—' ;
2014+ const allocCount = n . allocation_count ?? '—' ;
2015+ const isOnline = n . is_online ;
2016+ return ahtml `
2017+ <tr>
2018+ <td>${ n . id } </td>
2019+ <td><a href="/admin/node/${ n . id } " onclick="event.preventDefault();adminNavigateTo('node/${ n . id } ')" style="font-weight:600;cursor:pointer">${ escapeHtml ( n . name ) } </a></td>
2020+ <td><span class="server-detail-tag">${ escapeHtml ( n . fqdn ) } </span></td>
2021+ <td>${ ramMB !== '—' ? ramMB + ' MB' : '—' } </td>
2022+ <td>${ diskGB !== '—' ? diskGB + ' GB' : '—' } </td>
2023+ <td>${ cpuPct } </td>
2024+ <td>${ allocCount } </td>
2025+ <td>${ isOnline !== undefined ? ( isOnline ? '<span style="color:var(--accent-green)">Online</span>' : '<span style="color:var(--accent-red)">Offline</span>' ) : '—' } </td>
2026+ </tr>
2027+ ` ;
2028+ } ) . join ( '' ) ;
2029+ } catch ( err ) {
2030+ const tbody = $a ( '#admin-nodes-tbody' ) ;
2031+ if ( tbody ) tbody . innerHTML = `<tr><td colspan="8" style="text-align:center;padding:32px;color:var(--accent-red)">Error: ${ err . message } </td></tr>` ;
2032+ }
2033+ }
2034+
2035+ async function renderAdminNodeDetail ( nodeId ) {
2036+ document . querySelectorAll ( '.admin-page' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
2037+ const el = $a ( '#admin-page-node-detail' ) ;
2038+ if ( ! el ) return ;
2039+ el . classList . add ( 'active' ) ;
2040+ el . innerHTML = ahtml `
2041+ <div class="page-header">
2042+ <a href="/admin/nodes" onclick="event.preventDefault();adminNavigateTo('nodes')" class="btn btn-ghost btn-sm" style="display:inline-flex;width:auto;margin-bottom:16px">
2043+ <i data-lucide="arrow-left" style="width:14px;height:14px"></i>
2044+ Back to Nodes
2045+ </a>
2046+ <h1 class="page-title" style="margin-bottom:0">Node #${ nodeId } </h1>
2047+ <p class="page-subtitle" id="admin-node-name">Loading...</p>
2048+ </div>
2049+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px" id="admin-node-info-cards">
2050+ <div class="card" style="padding:20px">
2051+ <h3 style="color:var(--text-secondary);font-size:0.8rem;text-transform:uppercase;margin-bottom:12px">Details</h3>
2052+ <div id="admin-node-details"><span class="spinner"></span></div>
2053+ </div>
2054+ <div class="card" style="padding:20px">
2055+ <h3 style="color:var(--text-secondary);font-size:0.8rem;text-transform:uppercase;margin-bottom:12px">Resources</h3>
2056+ <div id="admin-node-resources"><span class="spinner"></span></div>
2057+ </div>
2058+ </div>
2059+ <div class="card" style="padding:20px;margin-bottom:24px">
2060+ <h3 style="color:var(--text-secondary);font-size:0.8rem;text-transform:uppercase;margin-bottom:12px">Allocations</h3>
2061+ <div class="table-container" style="margin:0">
2062+ <table>
2063+ <thead>
2064+ <tr>
2065+ <th>ID</th>
2066+ <th>IP</th>
2067+ <th>Port</th>
2068+ <th>Alias</th>
2069+ <th>Server</th>
2070+ </tr>
2071+ </thead>
2072+ <tbody id="admin-node-alloc-tbody">
2073+ <tr><td colspan="5" style="text-align:center;padding:16px;color:var(--text-secondary)"><span class="spinner"></span></td></tr>
2074+ </tbody>
2075+ </table>
2076+ </div>
2077+ </div>
2078+ <div class="card" style="padding:20px">
2079+ <h3 style="color:var(--text-secondary);font-size:0.8rem;text-transform:uppercase;margin-bottom:12px">Servers on this Node</h3>
2080+ <div class="table-container" style="margin:0">
2081+ <table>
2082+ <thead>
2083+ <tr>
2084+ <th>ID</th>
2085+ <th>Name</th>
2086+ <th>Identifier</th>
2087+ <th>Status</th>
2088+ </tr>
2089+ </thead>
2090+ <tbody id="admin-node-servers-tbody">
2091+ <tr><td colspan="4" style="text-align:center;padding:16px;color:var(--text-secondary)"><span class="spinner"></span></td></tr>
2092+ </tbody>
2093+ </table>
2094+ </div>
2095+ </div>
2096+ ` ;
2097+ initIcons ( ) ;
2098+
2099+ try {
2100+ const [ nodeData , allocData , serversData ] = await Promise . all ( [
2101+ adminApi ( `/nodes/${ nodeId } ` ) ,
2102+ adminApi ( `/nodes/${ nodeId } /allocations` ) . catch ( ( ) => ( { allocations : [ ] } ) ) ,
2103+ adminApi ( `/nodes/${ nodeId } /servers` ) . catch ( ( ) => ( { servers : [ ] } ) ) ,
2104+ ] ) ;
2105+
2106+ const n = nodeData . node ;
2107+ const nameEl = $a ( '#admin-node-name' ) ;
2108+ if ( nameEl ) nameEl . textContent = escapeHtml ( n . name ) ;
2109+
2110+ const detailsEl = $a ( '#admin-node-details' ) ;
2111+ if ( detailsEl ) {
2112+ detailsEl . innerHTML = ahtml `
2113+ <div style="display:grid;gap:8px;font-size:0.88rem">
2114+ <div><span style="color:var(--text-secondary)">FQDN:</span> <strong>${ escapeHtml ( n . fqdn ) } </strong></div>
2115+ <div><span style="color:var(--text-secondary)">Scheme:</span> ${ n . scheme || 'https' } </div>
2116+ <div><span style="color:var(--text-secondary)">Port:</span> ${ n . port || '—' } </div>
2117+ <div><span style="color:var(--text-secondary)">Daemon Token:</span> <span style="font-family:monospace;font-size:0.8rem">${ n . daemon_token ? '••••••••' : '—' } </span></div>
2118+ <div><span style="color:var(--text-secondary)">Status:</span> ${ n . is_online !== undefined ? ( n . is_online ? '<span style="color:var(--accent-green)">Online</span>' : '<span style="color:var(--accent-red)">Offline</span>' ) : '—' } </div>
2119+ <div><span style="color:var(--text-secondary)">Communication:</span> ${ n . communications || '—' } </div>
2120+ </div>
2121+ ` ;
2122+ }
2123+
2124+ const resEl = $a ( '#admin-node-resources' ) ;
2125+ if ( resEl ) {
2126+ const memTotal = n . memory ? Math . round ( n . memory / 1024 ) : 0 ;
2127+ const memUsed = n . memory_allocated ? Math . round ( n . memory_allocated / 1024 ) : 0 ;
2128+ const diskTotal = n . disk ? ( n . disk / 1024 ) . toFixed ( 1 ) : 0 ;
2129+ const diskUsed = n . disk_allocated ? ( n . disk_allocated / 1024 ) . toFixed ( 1 ) : 0 ;
2130+ resEl . innerHTML = ahtml `
2131+ <div style="display:grid;gap:12px;font-size:0.88rem">
2132+ <div>
2133+ <div style="display:flex;justify-content:space-between;margin-bottom:4px"><span>RAM</span><span>${ memUsed } / ${ memTotal } MB</span></div>
2134+ <div style="background:var(--bg-secondary);border-radius:4px;height:8px;overflow:hidden"><div style="background:var(--accent-1);height:100%;width:${ memTotal > 0 ? ( memUsed / memTotal * 100 ) : 0 } %;border-radius:4px"></div></div>
2135+ </div>
2136+ <div>
2137+ <div style="display:flex;justify-content:space-between;margin-bottom:4px"><span>Disk</span><span>${ diskUsed } / ${ diskTotal } GB</span></div>
2138+ <div style="background:var(--bg-secondary);border-radius:4px;height:8px;overflow:hidden"><div style="background:var(--accent-3);height:100%;width:${ diskTotal > 0 ? ( diskUsed / diskTotal * 100 ) : 0 } %;border-radius:4px"></div></div>
2139+ </div>
2140+ <div>
2141+ <div style="display:flex;justify-content:space-between;margin-bottom:4px"><span>CPU</span><span>${ n . cpu || 0 } %</span></div>
2142+ <div style="background:var(--bg-secondary);border-radius:4px;height:8px;overflow:hidden"><div style="background:var(--accent-2);height:100%;width:${ n . cpu || 0 } %;border-radius:4px"></div></div>
2143+ </div>
2144+ <div style="margin-top:4px"><span style="color:var(--text-secondary)">Allocation Count:</span> <strong>${ n . allocation_count ?? '—' } </strong></div>
2145+ </div>
2146+ ` ;
2147+ }
2148+
2149+ const allocTbody = $a ( '#admin-node-alloc-tbody' ) ;
2150+ if ( allocTbody ) {
2151+ const allocs = allocData . allocations || [ ] ;
2152+ if ( allocs . length === 0 ) {
2153+ allocTbody . innerHTML = '<tr><td colspan="5" style="text-align:center;padding:16px;color:var(--text-secondary)">No allocations found.</td></tr>' ;
2154+ } else {
2155+ allocTbody . innerHTML = allocs . map ( a => ahtml `
2156+ <tr>
2157+ <td>${ a . id } </td>
2158+ <td><span class="server-detail-tag">${ escapeHtml ( a . ip ) } </span></td>
2159+ <td>${ a . port } </td>
2160+ <td>${ a . alias ? escapeHtml ( a . alias ) : '—' } </td>
2161+ <td>${ a . server ? `<span style="color:var(--accent-1)">${ a . server } </span>` : '<span style="color:var(--text-secondary)">Free</span>' } </td>
2162+ </tr>
2163+ ` ) . join ( '' ) ;
2164+ }
2165+ }
2166+
2167+ const serversTbody = $a ( '#admin-node-servers-tbody' ) ;
2168+ if ( serversTbody ) {
2169+ const servers = serversData . servers || [ ] ;
2170+ if ( servers . length === 0 ) {
2171+ serversTbody . innerHTML = '<tr><td colspan="4" style="text-align:center;padding:16px;color:var(--text-secondary)">No servers on this node.</td></tr>' ;
2172+ } else {
2173+ serversTbody . innerHTML = servers . map ( s => ahtml `
2174+ <tr>
2175+ <td>${ s . id } </td>
2176+ <td>${ escapeHtml ( s . name ) } </td>
2177+ <td><span class="server-detail-tag">${ escapeHtml ( s . identifier ) } </span></td>
2178+ <td><span class="server-detail-tag">${ s . status || '—' } </span></td>
2179+ </tr>
2180+ ` ) . join ( '' ) ;
2181+ }
2182+ }
2183+ } catch ( err ) {
2184+ el . innerHTML = ahtml `
2185+ <div class="page-header">
2186+ <a href="/admin/nodes" onclick="event.preventDefault();adminNavigateTo('nodes')" class="btn btn-ghost btn-sm" style="display:inline-flex;width:auto;margin-bottom:16px">
2187+ <i data-lucide="arrow-left" style="width:14px;height:14px"></i>
2188+ Back to Nodes
2189+ </a>
2190+ <h1 class="page-title">Error</h1>
2191+ <p style="color:var(--accent-red)">${ err . message } </p>
2192+ </div>
2193+ ` ;
2194+ initIcons ( ) ;
2195+ }
2196+ }
2197+
19442198// ─── Common ─────────────────────────────────────────────
19452199function formatDateWithTooltip ( d ) {
19462200 if ( ! d ) return 'N/A' ;
@@ -1987,6 +2241,12 @@ window.addEventListener('popstate', () => {
19872241 if ( tabBtn ) tabBtn . click ( ) ;
19882242 } , 50 ) ;
19892243 }
2244+ } else if ( basePage === 'node' && param ) {
2245+ const nid = parseInt ( param , 10 ) ;
2246+ $a ( '#admin-page-node-detail' ) ?. classList . add ( 'active' ) ;
2247+ renderAdminNodeDetail ( nid ) ;
2248+ } else if ( basePage === 'nodes' ) {
2249+ adminNavigateTo ( 'nodes' ) ;
19902250 } else if ( basePage === 'users' ) {
19912251 adminNavigateTo ( 'users' ) ;
19922252 } else if ( basePage === 'dashboard' || ! basePage || basePage === 'login' ) {
0 commit comments