@@ -2046,61 +2046,99 @@ async function renderAdminNodeDetail(nodeId) {
20462046 <h1 class="page-title" style="margin-bottom:0">Node #${ nodeId } </h1>
20472047 <p class="page-subtitle" id="admin-node-name">Loading...</p>
20482048 </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>
2049+ <div class="tabs" id="admin-node-tabs">
2050+ <button class="tab active" data-tab="info">Info</button>
2051+ <button class="tab" data-tab="allocations">Allocations</button>
2052+ <button class="tab" data-tab="servers">Servers</button>
2053+ <button class="tab" data-tab="settings">Settings</button>
2054+ <div class="tab-indicator" id="admin-node-tab-indicator"></div>
2055+ </div>
2056+
2057+ <div id="admin-node-tab-info" class="tab-content" style="display:block">
2058+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
2059+ <div class="card" style="padding:20px">
2060+ <h3 style="color:var(--text-secondary);font-size:0.8rem;text-transform:uppercase;margin-bottom:12px">Details</h3>
2061+ <div id="admin-node-details"><span class="spinner"></span></div>
2062+ </div>
2063+ <div class="card" style="padding:20px">
2064+ <h3 style="color:var(--text-secondary);font-size:0.8rem;text-transform:uppercase;margin-bottom:12px">Resources</h3>
2065+ <div id="admin-node-resources"><span class="spinner"></span></div>
2066+ </div>
20532067 </div>
2068+ </div>
2069+
2070+ <div id="admin-node-tab-allocations" class="tab-content" style="display:none">
20542071 <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>
2072+ <div class="table-container" style="margin:0">
2073+ <table>
2074+ <thead>
2075+ <tr>
2076+ <th>ID</th>
2077+ <th>IP</th>
2078+ <th>Port</th>
2079+ <th>Alias</th>
2080+ <th>Server</th>
2081+ </tr>
2082+ </thead>
2083+ <tbody id="admin-node-alloc-tbody">
2084+ <tr><td colspan="5" style="text-align:center;padding:16px;color:var(--text-secondary)"><span class="spinner"></span></td></tr>
2085+ </tbody>
2086+ </table>
2087+ </div>
20572088 </div>
20582089 </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>
2090+
2091+ <div id="admin-node-tab-servers" class="tab-content" style="display:none">
2092+ <div class="card" style="padding:20px">
2093+ <div class="table-container" style="margin:0">
2094+ <table>
2095+ <thead>
2096+ <tr>
2097+ <th>ID</th>
2098+ <th>Name</th>
2099+ <th>Identifier</th>
2100+ <th>Status</th>
2101+ </tr>
2102+ </thead>
2103+ <tbody id="admin-node-servers-tbody">
2104+ <tr><td colspan="4" style="text-align:center;padding:16px;color:var(--text-secondary)"><span class="spinner"></span></td></tr>
2105+ </tbody>
2106+ </table>
2107+ </div>
20762108 </div>
20772109 </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>
2110+
2111+ <div id="admin-node-tab-settings" class="tab-content" style="display:none">
2112+ <div class="card" style="padding:24px;max-width:480px">
2113+ <h2 class="card-title" style="margin-bottom:16px">Node Availability</h2>
2114+ <p style="color:var(--text-secondary);font-size:0.88rem;margin-bottom:20px">
2115+ When a node is set to <strong>Unavailable</strong>, new servers will not be created on it. Existing servers are not affected.
2116+ </p>
2117+ <div id="admin-node-settings-form">
2118+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
2119+ <label class="toggle-switch" style="position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0">
2120+ <input type="checkbox" id="node-unavailable-toggle" style="opacity:0;width:0;height:0">
2121+ <span class="toggle-slider" style="position:absolute;cursor:pointer;inset:0;background:var(--bg-secondary);border:1px solid var(--border);border-radius:24px;transition:0.2s"></span>
2122+ </label>
2123+ <div>
2124+ <div id="node-unavailable-label" style="font-weight:600;font-size:0.9rem">Available</div>
2125+ <div style="color:var(--text-secondary);font-size:0.8rem">Node is accepting new server deployments</div>
2126+ </div>
2127+ </div>
2128+ <div id="node-settings-error" class="auth-error" style="margin-bottom:12px"></div>
2129+ <button class="btn btn-primary" id="btn-save-node-settings" style="width:auto">Save Settings</button>
2130+ </div>
20942131 </div>
20952132 </div>
20962133 ` ;
20972134 initIcons ( ) ;
20982135
20992136 try {
2100- const [ nodeData , allocData , serversData ] = await Promise . all ( [
2137+ const [ nodeData , allocData , serversData , settingsData ] = await Promise . all ( [
21012138 adminApi ( `/nodes/${ nodeId } ` ) ,
21022139 adminApi ( `/nodes/${ nodeId } /allocations` ) . catch ( ( ) => ( { allocations : [ ] } ) ) ,
21032140 adminApi ( `/nodes/${ nodeId } /servers` ) . catch ( ( ) => ( { servers : [ ] } ) ) ,
2141+ adminApi ( `/nodes/${ nodeId } /settings` ) . catch ( ( ) => ( { settings : { unavailable : false } } ) ) ,
21042142 ] ) ;
21052143
21062144 const n = nodeData . node ;
@@ -2180,6 +2218,39 @@ async function renderAdminNodeDetail(nodeId) {
21802218 ` ) . join ( '' ) ;
21812219 }
21822220 }
2221+
2222+ const toggle = $a ( '#node-unavailable-toggle' ) ;
2223+ const label = $a ( '#node-unavailable-label' ) ;
2224+ const isUnavailable = ! ! settingsData . settings ?. unavailable ;
2225+ if ( toggle ) {
2226+ toggle . checked = isUnavailable ;
2227+ updateNodeSettingsLabel ( isUnavailable ) ;
2228+ toggle . addEventListener ( 'change' , ( ) => updateNodeSettingsLabel ( toggle . checked ) ) ;
2229+ }
2230+
2231+ const saveBtn = $a ( '#btn-save-node-settings' ) ;
2232+ if ( saveBtn ) {
2233+ saveBtn . addEventListener ( 'click' , async ( ) => {
2234+ saveBtn . disabled = true ;
2235+ saveBtn . innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px"></span> Saving...' ;
2236+ const errEl = $a ( '#node-settings-error' ) ;
2237+ if ( errEl ) errEl . classList . remove ( 'show' ) ;
2238+ try {
2239+ await adminApi ( `/nodes/${ nodeId } /settings` , {
2240+ method : 'PUT' ,
2241+ body : JSON . stringify ( { unavailable : toggle . checked } ) ,
2242+ } ) ;
2243+ saveBtn . innerHTML = 'Saved!' ;
2244+ setTimeout ( ( ) => { saveBtn . disabled = false ; saveBtn . innerHTML = 'Save Settings' ; } , 1500 ) ;
2245+ } catch ( err ) {
2246+ if ( errEl ) { errEl . textContent = err . message ; errEl . classList . add ( 'show' ) ; }
2247+ saveBtn . disabled = false ;
2248+ saveBtn . innerHTML = 'Save Settings' ;
2249+ }
2250+ } ) ;
2251+ }
2252+
2253+ initNodeTabs ( nodeId ) ;
21832254 } catch ( err ) {
21842255 el . innerHTML = ahtml `
21852256 <div class="page-header">
@@ -2195,6 +2266,67 @@ async function renderAdminNodeDetail(nodeId) {
21952266 }
21962267}
21972268
2269+ function updateNodeSettingsLabel ( isUnavailable ) {
2270+ const label = $a ( '#node-unavailable-label' ) ;
2271+ const desc = label ?. nextElementSibling ;
2272+ if ( label ) {
2273+ label . textContent = isUnavailable ? 'Unavailable' : 'Available' ;
2274+ label . style . color = isUnavailable ? 'var(--accent-red)' : '' ;
2275+ }
2276+ if ( desc ) {
2277+ desc . textContent = isUnavailable
2278+ ? 'Node will be excluded from new server deployments'
2279+ : 'Node is accepting new server deployments' ;
2280+ }
2281+ }
2282+
2283+ function initNodeTabs ( nodeId ) {
2284+ const tabs = $a ( '#admin-node-tabs' ) ;
2285+ if ( ! tabs ) return ;
2286+ const indicator = $a ( '#admin-node-tab-indicator' ) ;
2287+ const btns = tabs . querySelectorAll ( '.tab' ) ;
2288+
2289+ function switchTab ( tabBtn ) {
2290+ const tabName = tabBtn . dataset . tab ;
2291+ btns . forEach ( t => t . classList . remove ( 'active' ) ) ;
2292+ tabBtn . classList . add ( 'active' ) ;
2293+
2294+ document . querySelectorAll ( '#admin-page-node-detail .tab-content' ) . forEach ( c => c . style . display = 'none' ) ;
2295+ const target = $a ( '#admin-node-tab-' + tabName ) ;
2296+ if ( target ) target . style . display = 'block' ;
2297+
2298+ if ( indicator ) {
2299+ indicator . style . left = tabBtn . offsetLeft + 'px' ;
2300+ indicator . style . width = tabBtn . offsetWidth + 'px' ;
2301+ }
2302+
2303+ history . pushState ( { adminPage : 'node' , nodeId, tab : tabName } , '' , `/admin/node/${ nodeId } /${ tabName } ` ) ;
2304+ }
2305+
2306+ btns . forEach ( btn => {
2307+ btn . addEventListener ( 'click' , ( ) => {
2308+ if ( btn . classList . contains ( 'active' ) ) return ;
2309+ switchTab ( btn ) ;
2310+ } ) ;
2311+ } ) ;
2312+
2313+ const pathParts = window . location . pathname . replace ( '/admin/' , '' ) . split ( '/' ) ;
2314+ const tabFromUrl = pathParts [ 2 ] ;
2315+ if ( tabFromUrl ) {
2316+ const tabBtn = Array . from ( btns ) . find ( b => b . dataset . tab === tabFromUrl ) ;
2317+ if ( tabBtn ) {
2318+ switchTab ( tabBtn ) ;
2319+ return ;
2320+ }
2321+ }
2322+
2323+ const activeBtn = tabs . querySelector ( '.tab.active' ) ;
2324+ if ( activeBtn && indicator ) {
2325+ indicator . style . left = activeBtn . offsetLeft + 'px' ;
2326+ indicator . style . width = activeBtn . offsetWidth + 'px' ;
2327+ }
2328+ }
2329+
21982330// ─── Common ─────────────────────────────────────────────
21992331function formatDateWithTooltip ( d ) {
22002332 if ( ! d ) return 'N/A' ;
@@ -2245,6 +2377,13 @@ window.addEventListener('popstate', () => {
22452377 const nid = parseInt ( param , 10 ) ;
22462378 $a ( '#admin-page-node-detail' ) ?. classList . add ( 'active' ) ;
22472379 renderAdminNodeDetail ( nid ) ;
2380+ const tab = pathParts [ 2 ] ;
2381+ if ( tab ) {
2382+ setTimeout ( ( ) => {
2383+ const tabBtn = document . querySelector ( '#admin-node-tabs .tab[data-tab="' + tab + '"]' ) ;
2384+ if ( tabBtn ) tabBtn . click ( ) ;
2385+ } , 50 ) ;
2386+ }
22482387 } else if ( basePage === 'nodes' ) {
22492388 adminNavigateTo ( 'nodes' ) ;
22502389 } else if ( basePage === 'users' ) {
0 commit comments