Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
Author[]Andy Green <andy@warmcat.com> 2025-09-19 10:55 UTC
Committer[]Andy Green <andy@warmcat.com> 2025-09-20 04:01 UTC
Tree929db913c7c47d368a121075552c7eb4cb9e7134   Raw Patch
 
feat: Add builder spreadsheet to web UI
feat: Add builder spreadsheet to web UI

Co-developed-by: Gemini 2.5 Pro
diff --git a/assets/index.html b/assets/index.html index c0b3423..9ce4186 100644 --- a/assets/index.html +++ b/assets/index.html @@ -9,47 +9,42 @@ </head> <body class="overlay-active"> <div class="overlay"></div> - <div class="summary nailed"> - <table><tr><td class="topregion"> - <div class="logo"> - <img class="logo" src="sai.svg"><br> - <span class="logo"><a href="https://warmcat.com/git/sai">Sai git</a></span> + <div class="main-container"> + <div class="left-pane"> + <div class="logo"> + <img class="logo" src="sai.svg"><br> + <span class="logo"><a href="https://warmcat.com/git/sai">Sai git</a></span> <span class="logo" id="apirev"></span> - <div> - <div id="login" class="login"> - <div id="creds" class="creds hide"> - <form class="login"> - <label for="lname">Name:</label> - <input class="crin" type="text" id="lname" name="lname" autocomplete="username"><br> - <label for="lpass">Password:</label> - <input class="crin" type="password" id="lpass" name="lpass" autocomplete="current-password"><br> - <input class="crinb" type="button" value="Login" id="login"> - <input type="hidden" id="success_redir" name="success_redir" value=""> - </form> </div> - <div id="logout" class="creds hide"> - <div id="remauth"></div> - <input class="crin" type="button" id="logout" value="Logout"> + <div id="login" class="login"> + <div id="creds" class="creds hide"> + <form class="login"> + <label for="lname">Name:</label> + <input class="crin" type="text" id="lname" name="lname" autocomplete="username"><br> + <label for="lpass">Password:</label> + <input class="crin" type="password" id="lpass" name="lpass" autocomplete="current-password"><br> + <input class="crinb" type="button" value="Login" id="login-button"> + <input type="hidden" id="success_redir" name="success_redir" value=""> + </form> + </div> + <div id="logout" class="creds hide"> + <div id="remauth"></div> + <input class="crin" type="button" id="logout-button" value="Logout"> + </div> </div> - </div> - </td><td class="topregion"> - - <div class="builder"> <div id="sai_builders"></div> </div> - </td></tr></table> - - </div> - <div id="sai_sticky" class="sticky"></div> - <div id="p2" class="undernailed"> - - - <td colspan=3 class="summary" id="summary"> - <noscript><span id="noscript" class="noscript">Please enable Javascript</span></noscript> - <div id="sai_overview"></div> - <div id="sai_task"></div> - </td> - </tr> + <div class="resizer" id="resizer"></div> + <div class="right-pane"> + <div id="sai_sticky" class="sticky"></div> + <div id="p2" class="undernailed"> + <div class="summary" id="summary"> + <noscript><span id="noscript" class="noscript">Please enable Javascript</span></noscript> + <div id="sai_overview"></div> + <div id="sai_task"></div> + </div> + </div> + </div> </div> </body> </html> diff --git a/assets/sai.css b/assets/sai.css index f825a86..14bc7cf 100644 --- a/assets/sai.css +++ b/assets/sai.css @@ -988,3 +988,40 @@ img.branch { body.overlay-active { overflow: hidden; } + +.spreadsheet { + margin-top: 5px; +} +.spreadsheet table { + width: 100%; + border-collapse: collapse; +} +.spreadsheet th, .spreadsheet td { + border: 1px solid #ddd; + padding: 4px; + font-size: 7pt; + text-align: left; +} +.spreadsheet th { + background-color: #f2f2f2; +} + +.main-container { + display: flex; + height: 100vh; +} +.left-pane { + flex: 0 0 30%; + overflow: auto; + flex-shrink: 0; +} +.right-pane { + flex-grow: 1; + overflow: auto; +} +.resizer { + width: 5px; + cursor: ew-resize; + background-color: #ccc; + flex-shrink: 0; +} diff --git a/assets/sai.js b/assets/sai.js index 2df3e43..6d72a5d 100644 --- a/assets/sai.js +++ b/assets/sai.js @@ -1254,33 +1254,12 @@ function ws_open_sai() // --- Reconciliation Logic --- - // Step 1: Find the main content area (the TD). Create it if this is the first run. - let tdContainer = buildersContainer.querySelector("table td"); - if (!tdContainer) { - buildersContainer.innerHTML = ""; // Clear for safety - const table = document.createElement("table"); - table.className = "builders"; - const tbody = document.createElement("tbody"); - const tr = document.createElement("tr"); - tdContainer = document.createElement("td"); - tr.appendChild(tdContainer); - tbody.appendChild(tr); - table.appendChild(tbody); - buildersContainer.appendChild(table); - } - - // Step 2: Detach all existing builder divs and store them in a map for reuse. - // This preserves them so their CSS transitions will work. - const existingDivs = new Map(); - tdContainer.querySelectorAll(".bdr").forEach(div => { - const name = div.id.substring(6); // "binfo-" is 6 chars - if (name) { - existingDivs.set(name, div); - } - }); - tdContainer.innerHTML = ""; // Clear the container, but the divs are still in memory. + buildersContainer.innerHTML = ""; // Clear for safety + const table = document.createElement("table"); + table.className = "builders"; + const tbody = document.createElement("tbody"); + table.appendChild(tbody); - // Step 3: Rebuild the group structure from scratch, reusing the old divs. platformsArray.sort((a, b) => { if (a.name && b.name) { return a.name.localeCompare(b.name); @@ -1288,46 +1267,24 @@ function ws_open_sai() return 0; // Don't sort if names are missing }); - const platformsByGroup = {}; for (const plat of platformsArray) { - const groupKey = getBuilderGroupKey(plat.name); - if (!platformsByGroup[groupKey]) platformsByGroup[groupKey] = []; - platformsByGroup[groupKey].push(plat); + const tr = document.createElement("tr"); + + const tdInfo = document.createElement("td"); + tdInfo.className = "builder-info"; + const builderDiv = createBuilderDiv(plat); + tdInfo.appendChild(builderDiv); + tr.appendChild(tdInfo); + + const tdSpreadsheet = document.createElement("td"); + tdSpreadsheet.className = "spreadsheet-container"; + tdSpreadsheet.id = "spreadsheet-" + plat.name; + tr.appendChild(tdSpreadsheet); + + tbody.appendChild(tr); } - const groupKeys = Object.keys(platformsByGroup).sort(); - for (const key of groupKeys) { - const groupPlatforms = platformsByGroup[key]; - const nestedPlatforms = groupPlatforms.filter(p => getBuilderHostname(p.name) !== key); - const mainPlatforms = groupPlatforms.filter(p => getBuilderHostname(p.name) === key); - - if (nestedPlatforms.length > 0) { - const groupDiv = document.createElement("div"); - groupDiv.className = "ibuil ibuilctr bdr"; - - const nameDiv = document.createElement("div"); - nameDiv.className = "ibuilctrname bdr"; - nameDiv.textContent = key; - groupDiv.appendChild(nameDiv); - - nestedPlatforms.forEach(plat => { - const div = existingDivs.get(plat.name) || createBuilderDiv(plat); - div.classList.toggle('offline', !plat.online); - div.classList.toggle('powering-up', !!plat.powering_up); - div.classList.toggle('powering-down', !!plat.powering_down); - groupDiv.appendChild(div); - }); - tdContainer.appendChild(groupDiv); - } - - mainPlatforms.forEach(plat => { - const div = existingDivs.get(plat.name) || createBuilderDiv(plat); - div.classList.toggle('offline', !plat.online); - div.classList.toggle('powering-up', !!plat.powering_up); - div.classList.toggle('powering-down', !!plat.powering_down); - tdContainer.appendChild(div); - }); - } + buildersContainer.appendChild(table); break; case "com.warmcat.sai.build-metric": @@ -1673,6 +1630,30 @@ function ws_open_sai() if (cpu_percentage < 0) cpu_percentage = 0; barDiv.style.height = `${cpu_percentage}%`; } + + const spreadsheetContainer = document.getElementById("spreadsheet-" + jso.builder_name); + if (spreadsheetContainer) { + let table = '<table><thead><tr>' + + '<th>Task</th><th>Step</th><th>Started</th>' + + '<th>Mem (KiB)</th><th>CPU (%)</th><th>Disk (KiB)</th>' + + '</tr></thead><tbody>'; + + if (jso.active_tasks) { + jso.active_tasks.forEach(task => { + table += `<tr>` + + `<td>${hsanitize(task.task_name)}</td>` + + `<td>${hsanitize(task.build_step)}</td>` + + `<td>${agify(now_ut, task.started)}</td>` + + `<td>${humanize(task.est_peak_mem_kib * 1024)}</td>` + + `<td>${task.est_cpu_load_pct}</td>` + + `<td>${humanize(task.est_disk_kib * 1024)}</td>` + + `</tr>`; + }); + } + + table += '</tbody></table>'; + spreadsheetContainer.innerHTML = table; + } break; case "com-warmcat-sai-artifact": @@ -1849,6 +1830,15 @@ function post_login_form() /* stuff that has to be delayed until all the page assets are loaded */ window.addEventListener("load", function() { + + const savedFlex = localStorage.getItem('sai-left-pane-flex'); + if (savedFlex) { + const leftPane = document.querySelector('.left-pane'); + if (leftPane) { + leftPane.style.flex = savedFlex; + } + } + const lnameInput = document.getElementById("lname"); const lpassInput = document.getElementById("lpass"); @@ -1876,9 +1866,9 @@ window.addEventListener("load", function() { ws_open_sai(); aging(); - if (document.getElementById("login")) { - document.getElementById("login").addEventListener("click", post_login_form); - document.getElementById("logout").addEventListener("click", post_login_form); + if (document.getElementById("login-button")) { + document.getElementById("login-button").addEventListener("click", post_login_form); + document.getElementById("logout-button").addEventListener("click", post_login_form); } setInterval(function() { @@ -1952,7 +1942,34 @@ window.addEventListener("load", function() { } }); } - + const resizer = document.getElementById('resizer'); + if (resizer) { + const leftPane = resizer.previousElementSibling; + + let x = 0; + let leftWidth = 0; + + const onMouseMove = (e) => { + const dx = e.clientX - x; + const newLeftWidth = leftWidth + dx; + leftPane.style.flex = `0 0 ${newLeftWidth}px`; + }; + + const onMouseUp = () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + localStorage.setItem('sai-left-pane-flex', leftPane.style.flex); + }; + + const onMouseDown = (e) => { + x = e.clientX; + leftWidth = leftPane.getBoundingClientRect().width; + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; + + resizer.addEventListener('mousedown', onMouseDown); + } }, false); }()); diff --git a/src/builder/b-comms.c b/src/builder/b-comms.c index edce574..c17470a 100644 --- a/src/builder/b-comms.c +++ b/src/builder/b-comms.c @@ -169,6 +169,14 @@ saib_m_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len, n = (int)w; + lws_start_foreach_dll_safe(struct lws_dll2 *, p, p1, + lr->active_tasks.head) { + sai_active_task_info_t *ati = lws_container_of(p, + sai_active_task_info_t, list); + lws_dll2_remove(&ati->list); + free(ati); + } lws_end_foreach_dll_safe(p, p1); + lws_dll2_remove(&lr->list); free(lr); @@ -497,19 +505,30 @@ saib_sul_load_report_cb(struct lws_sorted_usec_list *sul) lr->free_disk_kib = saib_get_free_disk_kib(builder.home); lr->cpu_percent = (unsigned int)saib_get_system_cpu(&builder); lr->active_steps = 0; + lws_dll2_owner_clear(&lr->active_tasks); lws_start_foreach_dll(struct lws_dll2 *, p, builder.sai_plat_owner.head) { sp = lws_container_of(p, sai_plat_t, sai_plat_list); lws_start_foreach_dll(struct lws_dll2 *, d, sp->nspawn_owner.head) { struct sai_nspawn *ns = lws_container_of(d, struct sai_nspawn, list); - if (ns->state == NSSTATE_EXECUTING_STEPS) { - lr->active_steps++; + if (ns->state == NSSTATE_EXECUTING_STEPS && ns->task) { + sai_active_task_info_t *ati = malloc(sizeof(*ati)); + if (ati) { + memset(ati, 0, sizeof(*ati)); + lws_strncpy(ati->task_uuid, ns->task->uuid, sizeof(ati->task_uuid)); + lws_strncpy(ati->task_name, ns->task->taskname, sizeof(ati->task_name)); + ati->build_step = ns->current_step; + ati->est_peak_mem_kib = ns->task->est_peak_mem_kib; + ati->est_cpu_load_pct = ns->task->est_cpu_load_pct; + ati->est_disk_kib = ns->task->est_disk_kib; + ati->started = ns->task->started; + lws_dll2_add_tail(&ati->list, &lr->active_tasks); + lr->active_steps++; + } somebody_not_idle = 1; } } lws_end_foreach_dll(d); - -// lws_dll2_add_tail(&pl->list, &lr->platforms); } lws_end_foreach_dll(p); lws_dll2_add_tail(&lr->list, &spm->load_report_owner); @@ -609,7 +628,8 @@ saib_m_state(void *userobj, void *sh, lws_ss_constate_t state, lwsl_ss_user(spm->ss, "CONNECTED"); spm->phase = PHASE_START_ATTACH; /* Initialize the load report SUL timer for this server connection */ - lws_sul_cancel(&spm->sul_load_report); + lws_sul_schedule(builder.context, 0, &spm->sul_load_report, + saib_sul_load_report_cb, 1); return lws_ss_request_tx(spm->ss); diff --git a/src/common/include/private.h b/src/common/include/private.h index 76d1c40..39d1264 100644 --- a/src/common/include/private.h +++ b/src/common/include/private.h @@ -63,6 +63,17 @@ enum { }; /* The top-level load report message from a builder */ +typedef struct sai_active_task_info { + lws_dll2_t list; + char task_uuid[65]; + char task_name[96]; + int build_step; + unsigned int est_peak_mem_kib; + unsigned int est_cpu_load_pct; + unsigned int est_disk_kib; + uint64_t started; +} sai_active_task_info_t; + typedef struct sai_load_report { lws_dll2_t list; /* For queuing on sai_plat_server */ char builder_name[64]; @@ -71,6 +82,7 @@ typedef struct sai_load_report { unsigned int free_disk_kib; unsigned int active_steps; unsigned int cpu_percent; + lws_dll2_owner_t active_tasks; } sai_load_report_t; /* @@ -542,8 +554,9 @@ extern const lws_struct_map_t lsm_rebuild[1], lsm_schema_rebuild[1], lsm_schema_build_metric[1], - lsm_schema_sq3_map_build_metric[1] -; + lsm_schema_sq3_map_build_metric[1], + lsm_load_report_members[7] + ; extern const lws_struct_map_t lsm_build_metric[12]; extern const lws_struct_map_t lsm_plat[8]; extern const lws_struct_map_t lsm_plat_for_json[11]; diff --git a/src/common/struct-metadata.c b/src/common/struct-metadata.c index 062e5a0..dbc8a9f 100644 --- a/src/common/struct-metadata.c +++ b/src/common/struct-metadata.c @@ -21,6 +21,16 @@ * lws_struct metadata for structs common to builder and server */ +const lws_struct_map_t lsm_active_task_info[] = { + LSM_CARRAY (sai_active_task_info_t, task_uuid, "task_uuid"), + LSM_CARRAY (sai_active_task_info_t, task_name, "task_name"), + LSM_SIGNED (sai_active_task_info_t, build_step, "build_step"), + LSM_UNSIGNED (sai_active_task_info_t, est_peak_mem_kib, "est_peak_mem_kib"), + LSM_UNSIGNED (sai_active_task_info_t, est_cpu_load_pct, "est_cpu_load_pct"), + LSM_UNSIGNED (sai_active_task_info_t, est_disk_kib, "est_disk_kib"), + LSM_UNSIGNED (sai_active_task_info_t, started, "started"), +}; + const lws_struct_map_t lsm_load_report_members[] = { LSM_CARRAY (sai_load_report_t, builder_name, "builder_name"), LSM_SIGNED (sai_load_report_t, core_count, "core_count"), @@ -28,8 +38,8 @@ const lws_struct_map_t lsm_load_report_members[] = { LSM_UNSIGNED (sai_load_report_t, free_disk_kib, "free_disk_kib"), LSM_UNSIGNED (sai_load_report_t, active_steps, "active_steps"), LSM_UNSIGNED (sai_load_report_t, cpu_percent, "cpu_percent"), -// LSM_LIST (sai_load_report_t, platforms, sai_platform_load_t, list, -// NULL, lsm_platform_load, "platforms"), + LSM_LIST (sai_load_report_t, active_tasks, sai_active_task_info_t, list, + NULL, lsm_active_task_info, "active_tasks"), }; const lws_struct_map_t lsm_build_metric[] = { diff --git a/src/server/s-ws-builder.c b/src/server/s-ws-builder.c index 47779e6..f5469a4 100644 --- a/src/server/s-ws-builder.c +++ b/src/server/s-ws-builder.c @@ -45,24 +45,6 @@ typedef struct sais_logcache_pertask { lws_dll2_owner_t cache; /* sai_log_t */ } sais_logcache_pertask_t; -/* map for a single instance's load */ -//static const lws_struct_map_t lsm_instance_load[] = { -// LSM_UNSIGNED(sai_instance_load_t, state, "state"), -//}; - -// static const lws_struct_map_t lsm_platform_load[] = { -// LSM_CARRAY(sai_platform_load_t, platform_name, "platform_name"), -// LSM_LIST(sai_platform_load_t, loads, sai_instance_load_t, list, -// NULL, lsm_instance_load, "loads"), -//}; - -/* map for the members of the load report object */ -static const lws_struct_map_t lsm_load_report_members[] = { - LSM_CARRAY(sai_load_report_t, builder_name, "builder_name"), -// LSM_LIST (sai_load_report_t, platforms, sai_platform_load_t, list, -// NULL, lsm_platform_load, "platforms"), -}; - /* * The Schema that may be sent to us by a builder * @@ -532,8 +514,6 @@ handle: * builder is sending us an array of platforms it provides us */ - lwsl_notice("%s: &&&&&&&&&&&&&&&&&&&&&\n", __func__); lwsl_hexdump_notice(buf, bl); - pss->u.o = (sai_plat_owner_t *)pss->a.dest; lws_start_foreach_dll(struct lws_dll2 *, pb, @@ -541,9 +521,6 @@ handle: build = lws_container_of(pb, sai_plat_t, sai_plat_list); sai_plat_t *live_cb; - lwsl_notice("%s: ***************** platform %s, windows %d\n", __func__, - build->name, build->windows); - /* * Step 1: Upsert this platform into the persistent database. */ @@ -727,9 +704,13 @@ bail: { sai_load_report_t *lr = (sai_load_report_t *)pss->a.dest; - lwsl_notice("%s: loadreport from %s: ram %uk, disk %uk\n", + lwsl_notice("%s: @@@@@@@@@@@@@@@@@@ loadreport from %s: ram %uk, disk %uk\n", __func__, lr->builder_name, lr->free_ram_kib, lr->free_disk_kib); + + ssize_t wr = write(2, buf, bl); + if (wr != (ssize_t)bl) + lwsl_notice("%s: write failed\n", __func__); } // lwsl_wsi_user(pss->wsi, "SAIM_WSSCH_BUILDER_LOADREPORT broadcasting\n"); sais_websrv_broadcast(vhd->h_ss_websrv, (const char *)buf, bl); diff --git a/src/web/w-websrv.c b/src/web/w-websrv.c index 6a555b6..0d32f3a 100644 --- a/src/web/w-websrv.c +++ b/src/web/w-websrv.c @@ -225,11 +225,11 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags) case SAIS_WS_WEBSRV_RX_LOADREPORT: /* Forward the final fragment of the load report */ - saiw_ws_broadcast_raw(vhd, buf, len, 0, + saiw_ws_broadcast_raw(vhd, buf, len - (unsigned int)n, 0, lws_write_ws_flags(LWS_WRITE_TEXT, flags & LWSSS_FLAG_SOM, flags & LWSSS_FLAG_EOM)); break; case SAIS_WS_WEBSRV_RX_TASKACTIVITY: - saiw_ws_broadcast_raw(vhd, buf, len, 0, + saiw_ws_broadcast_raw(vhd, buf, len - (unsigned int)n, 0, lws_write_ws_flags(LWS_WRITE_TEXT, flags & LWSSS_FLAG_SOM, flags & LWSSS_FLAG_EOM)); break; }
Page fetched 0s ago, creation time: 27ms (vhost etag hits: 0%, cache hits: 0%)