diff --git a/assets/index.html b/assets/index.html
index 53d5845..48ce7ea 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -20,6 +20,10 @@
</div>
<div id="lws-login-status-container" class="login-status-container"></div>
</div>
+ <div id="sai_power_overview" class="power-overview hidden">
+ <div class="power-text">Total Power: <span id="sai_total_power">0W</span></div>
+ <canvas id="sai_power_graph" class="power-graph" height="20" width="150"></canvas>
+ </div>
<div id="sai_builders"></div>
</div>
<div class="resizer" id="resizer"></div>
diff --git a/assets/sai.css b/assets/sai.css
index 35d5e7b..60109bf 100644
--- a/assets/sai.css
+++ b/assets/sai.css
@@ -1260,3 +1260,30 @@ body.overlay-active {
overflow: hidden;
text-overflow: ellipsis;
}
+
+div.power-overview {
+ padding: 5px 10px;
+ margin-bottom: 10px;
+ background: rgba(0,0,0,0.05);
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+div.power-overview.hidden {
+ display: none;
+}
+div.power-text {
+ font-weight: bold;
+ font-size: 0.7em;
+ white-space: nowrap;
+ margin-right: 10px;
+}
+canvas.power-graph {
+ width: 150px;
+ height: 20px;
+ display: block;
+ border: 1px solid #ccc;
+ background: #fff;
+ box-sizing: border-box;
+}
diff --git a/assets/sai.js b/assets/sai.js
index 6f2675b..2aa519d 100644
--- a/assets/sai.js
+++ b/assets/sai.js
@@ -1796,6 +1796,61 @@ function renderPconHierarchy(container) {
}
}
+let sai_power_samples = [];
+let sai_max_total_power_w = 0;
+
+function updatePowerGraph(total_w) {
+ const overviewDiv = document.getElementById("sai_power_overview");
+ const powerText = document.getElementById("sai_total_power");
+ const canvas = document.getElementById("sai_power_graph");
+
+ if (!overviewDiv || !powerText || !canvas) return;
+
+ if (overviewDiv.classList.contains("hidden")) {
+ overviewDiv.classList.remove("hidden");
+ }
+
+ /* if no updates passed (just drawing from memory), avoid adding a new sample */
+ if (total_w !== null) {
+ powerText.textContent = total_w + "W";
+
+ if (total_w > sai_max_total_power_w) {
+ sai_max_total_power_w = total_w;
+ }
+
+ sai_power_samples.push(total_w);
+ } else {
+ if (sai_power_samples.length > 0)
+ powerText.textContent = sai_power_samples[sai_power_samples.length - 1] + "W";
+ }
+
+ const ctx = canvas.getContext("2d");
+ if (canvas.width !== canvas.clientWidth) {
+ canvas.width = canvas.clientWidth;
+ }
+
+ const w = canvas.width;
+ const h = canvas.height;
+
+ if (sai_power_samples.length > w) {
+ sai_power_samples.shift();
+ }
+
+ ctx.clearRect(0, 0, w, h);
+ if (sai_power_samples.length === 0) return;
+
+ const max_y = sai_max_total_power_w > 0 ? sai_max_total_power_w : 1;
+
+ ctx.fillStyle = "#ff4136";
+ const startX = w - sai_power_samples.length;
+
+ for (let i = 0; i < sai_power_samples.length; i++) {
+ const val = sai_power_samples[i];
+ const barH = (val / max_y) * h;
+ ctx.fillRect(startX + i, h - barH, 1, barH);
+ }
+}
+
function ws_open_sai()
{
var s = "", q, qa, qi, q5, q5s;
@@ -2044,9 +2099,27 @@ function ws_open_sai()
stats.textContent = `${d.active_power_w}W`;
}
});
+
+ let total_w = 0;
+ for (const p in pcon_energy_cache) {
+ if (pcon_energy_cache[p].active_power_w)
+ total_w += pcon_energy_cache[p].active_power_w;
+ }
+ updatePowerGraph(total_w);
}
break;
+ case "com.warmcat.sai.power_history":
+ sai_max_total_power_w = jso.max_w;
+ sai_power_samples = [];
+ if (jso.samples) {
+ for (var j = 0; j < jso.samples.length; j++) {
+ sai_power_samples.push(jso.samples[j]);
+ }
+ }
+ updatePowerGraph(null); /* Redraw instantly without new sample */
+ break;
+
case "com.warmcat.sai.build-metric":
var summaryDiv = document.getElementById("metrics-summary-" + jso.task_uuid);
if (summaryDiv) {
@@ -2097,25 +2170,31 @@ function ws_open_sai()
* display events wholesale
*/
if (jso.overview.length) {
- var pagination_html = "";
+ var pagination_html_top = "";
+ var pagination_html_bottom = "";
if (!gitohashi_integ && jso.total_events > 6) {
- pagination_html += "<div class=\"sai-pagination\">";
+ var info_span = "<span class=\"sai-pagination-info\">Showing " + (jso.offset + 1) + " - " + Math.min(jso.offset + 6, jso.total_events) + " of " + jso.total_events + "</span>";
+
+ pagination_html_top += "<div class=\"sai-pagination\">";
if (jso.offset > 0) {
- pagination_html += "<button class=\"btn sai-pagination-btn\" data-offset=\"" + Math.max(0, jso.offset - 6) + "\">< Newer</button> ";
+ pagination_html_top += "<button class=\"btn sai-pagination-btn\" data-offset=\"" + Math.max(0, jso.offset - 6) + "\">< Newer</button> ";
}
- pagination_html += "<span class=\"sai-pagination-info\">Showing " + (jso.offset + 1) + " - " + Math.min(jso.offset + 6, jso.total_events) + " of " + jso.total_events + "</span>";
+ pagination_html_top += info_span + "</div>";
+
+ pagination_html_bottom += "<div class=\"sai-pagination\">";
+ pagination_html_bottom += info_span;
if (jso.offset + 6 < jso.total_events) {
- pagination_html += "<button class=\"btn sai-pagination-btn\" data-offset=\"" + (jso.offset + 6) + "\">Older ></button>";
+ pagination_html_bottom += " <button class=\"btn sai-pagination-btn\" data-offset=\"" + (jso.offset + 6) + "\">Older ></button>";
}
- pagination_html += "</div>";
+ pagination_html_bottom += "</div>";
}
- s = pagination_html + s;
+ s = pagination_html_top + s;
for (n = jso.overview.length - 1; n >= 0; n--)
s += sai_event_render(jso.overview[n], now_ut, 1);
- s = s + "</table>" + pagination_html;
+ s = s + "</table>" + pagination_html_bottom;
if (document.getElementById("sai_sticky"))
document.getElementById("sai_sticky").innerHTML = s;
diff --git a/src/web/w-comms.c b/src/web/w-comms.c
index 84f3ea9..f198fc6 100644
--- a/src/web/w-comms.c
+++ b/src/web/w-comms.c
@@ -185,7 +185,19 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user,
sai_sqlite3_statement(vhd->pdb, "CREATE UNIQUE INDEX IF NOT EXISTS idx_event_uuid ON events(uuid);", "create event index");
-
+ sai_sqlite3_statement(vhd->pdb,
+ "CREATE TABLE IF NOT EXISTS saiweb_state (key TEXT PRIMARY KEY, val INTEGER);",
+ "create saiweb_state");
+
+ {
+ sqlite3_stmt *stmt;
+ if (sqlite3_prepare_v2(vhd->pdb,
+ "SELECT val FROM saiweb_state WHERE key='max_power'", -1, &stmt, NULL) == SQLITE_OK) {
+ if (sqlite3_step(stmt) == SQLITE_ROW)
+ vhd->max_total_power_w = (unsigned int)sqlite3_column_int(stmt, 0);
+ sqlite3_finalize(stmt);
+ }
+ }
/*
* Reach out to the sai-server part over the SS ws websrv link
@@ -539,7 +551,7 @@ http_resp:
lws_dll2_remove(&pss->subs_list);
lws_sul_cancel(&pss->sul_logcache);
- for (n = 0; n < 3; n++) {
+ for (n = 0; n < 4; n++) {
if (pss->last_bps[n])
free(pss->last_bps[n]);
}
diff --git a/src/web/w-private.h b/src/web/w-private.h
index b2ba540..bdecaab 100644
--- a/src/web/w-private.h
+++ b/src/web/w-private.h
@@ -111,8 +111,8 @@ struct pss {
uint64_t artifact_offset;
uint64_t artifact_length;
- char *last_bps[3];
- size_t last_bps_len[3];
+ char *last_bps[4];
+ size_t last_bps_len[4];
unsigned int spa_failed:1;
unsigned int dry:1;
@@ -140,6 +140,11 @@ struct vhd {
lws_dll2_owner_t web_to_srv_owner;
lws_dll2_owner_t subs_owner;
sqlite3 *pdb;
+
+ lws_dll2_owner_t pcon_watts_owner;
+ unsigned int power_history[150];
+ int power_history_count;
+ unsigned int max_total_power_w;
struct lws_ss_handle *h_ss_websrv; /* client */
@@ -239,6 +244,10 @@ int
saiw_browser_broadcast_queue_pcons(struct vhd *vhd, struct pss *pss);
int
saiw_browser_broadcast_queue_pcon_energy(struct vhd *vhd, struct pss *pss, sai_pcon_energy_report_t *energy);
+void
+saiw_update_global_power_history(struct vhd *vhd, sai_pcon_energy_report_t *energy);
+int
+saiw_browser_broadcast_queue_power_history(struct vhd *vhd, struct pss *pss);
extern const lws_struct_map_t lsm_schema_pcon_energy[];
diff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c
index b0ae98d..fef69ef 100644
--- a/src/web/w-ws-browser.c
+++ b/src/web/w-ws-browser.c
@@ -1405,6 +1405,8 @@ saiw_browser_broadcast_queue_builders(struct vhd *vhd, struct pss *pss)
}
lwsac_unreference(&vhd->builders);
+
+ saiw_browser_broadcast_queue_power_history(vhd, pss);
return saiw_dedup_and_queue(pss, 0, &d);
}
@@ -1429,3 +1431,115 @@ saiw_browser_state_changed(struct pss *pss, int established)
}
+
+
+typedef struct pcon_watts {
+ lws_dll2_t list;
+ char name[64];
+ unsigned int active_power_w;
+} pcon_watts_t;
+
+void
+saiw_update_global_power_history(struct vhd *vhd, sai_pcon_energy_report_t *energy)
+{
+ unsigned int total_w;
+ char buf[1024];
+
+ if (!vhd || !energy)
+ return;
+
+ /* Record or update individual PCON wattages */
+ lws_start_foreach_dll(struct lws_dll2 *, p, energy->items.head) {
+ sai_pcon_energy_report_item_t *item = lws_container_of(p, sai_pcon_energy_report_item_t, list);
+ pcon_watts_t *pw = NULL;
+
+ lws_start_foreach_dll(struct lws_dll2 *, pt, vhd->pcon_watts_owner.head) {
+ pcon_watts_t *pw_iter = lws_container_of(pt, pcon_watts_t, list);
+ if (!strcmp(pw_iter->name, item->name)) {
+ pw = pw_iter;
+ break;
+ }
+ } lws_end_foreach_dll(pt);
+
+ if (!pw) {
+ pw = malloc(sizeof(*pw));
+ if (pw) {
+ memset(pw, 0, sizeof(*pw));
+ lws_strncpy(pw->name, item->name, sizeof(pw->name));
+ lws_dll2_add_tail(&pw->list, &vhd->pcon_watts_owner);
+ }
+ }
+ if (pw)
+ pw->active_power_w = item->data.active_power_w;
+
+ } lws_end_foreach_dll(p);
+
+ /* Calculate global watts across all tracked PCONs */
+ total_w = 0;
+ lws_start_foreach_dll(struct lws_dll2 *, pt, vhd->pcon_watts_owner.head) {
+ pcon_watts_t *pw = lws_container_of(pt, pcon_watts_t, list);
+ total_w += pw->active_power_w;
+ } lws_end_foreach_dll(pt);
+
+ /* Record this sample */
+ if (vhd->power_history_count == 150) {
+ memmove(vhd->power_history, vhd->power_history + 1, sizeof(unsigned int) * 149);
+ vhd->power_history[149] = total_w;
+ } else {
+ vhd->power_history[vhd->power_history_count++] = total_w;
+ }
+
+ if (total_w > vhd->max_total_power_w) {
+ vhd->max_total_power_w = total_w;
+ if (vhd->pdb) {
+ lws_snprintf(buf, sizeof(buf), "INSERT OR REPLACE INTO saiweb_state (key, val) VALUES ('max_power', %u)", total_w);
+ sai_sqlite3_statement(vhd->pdb, buf, "update max_power");
+ }
+ }
+}
+
+int
+saiw_browser_broadcast_queue_power_history(struct vhd *vhd, struct pss *pss)
+{
+ struct sai_dyn_buf d;
+ char buf[2048];
+ int n, i;
+
+ if (!vhd)
+ return 0;
+
+ memset(&d, 0, sizeof(d));
+
+ /* Reserve LWS_PRE header space */
+ memset(buf, 0, LWS_PRE);
+ if (sai_dyn_buf_append(&d, buf, LWS_PRE))
+ return 1;
+
+ n = lws_snprintf(buf, sizeof(buf),
+ "{\"schema\":\"com.warmcat.sai.power_history\","
+ " \"max_w\":%u,"
+ " \"samples\":[", vhd->max_total_power_w);
+
+ if (sai_dyn_buf_append(&d, buf, (size_t)n)) {
+ free(d.buf);
+ return 1;
+ }
+
+ for (i = 0; i < vhd->power_history_count; i++) {
+ n = lws_snprintf(buf, sizeof(buf), "%s%u",
+ i ? "," : "",
+ vhd->power_history[i]);
+ if (sai_dyn_buf_append(&d, buf, (size_t)n)) {
+ free(d.buf);
+ return 1;
+ }
+ }
+
+ n = lws_snprintf(buf, sizeof(buf), "]}");
+ if (sai_dyn_buf_append(&d, buf, (size_t)n)) {
+ free(d.buf);
+ return 1;
+ }
+
+ return saiw_dedup_and_queue(pss, 3, &d);
+}
diff --git a/src/web/w-ws-server.c b/src/web/w-ws-server.c
index 97036b4..e31a7e4 100644
--- a/src/web/w-ws-server.c
+++ b/src/web/w-ws-server.c
@@ -227,9 +227,11 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
break;
case SAIS_WS_WEBSRV_RX_PCON_ENERGY:
+ saiw_update_global_power_history(vhd, (sai_pcon_energy_report_t *)m->a.dest);
lws_start_foreach_dll(struct lws_dll2 *, p, vhd->browsers.head) {
struct pss *pss = lws_container_of(p, struct pss, same);
+ saiw_browser_broadcast_queue_power_history(pss->vhd, pss);
saiw_browser_broadcast_queue_pcon_energy(pss->vhd, pss, (sai_pcon_energy_report_t *)m->a.dest);
} lws_end_foreach_dll(p);
break;