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;
}