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> 2026-04-11 16:44 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-04-12 07:57 UTC
Tree4e0e447b1aab0e2d24c38629a9e1de12fbf6600c   Raw Patch
 
power-graph
power-graph
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) + "\">&lt; Newer</button> "; + pagination_html_top += "<button class=\"btn sai-pagination-btn\" data-offset=\"" + Math.max(0, jso.offset - 6) + "\">&lt; 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 &gt;</button>"; + pagination_html_bottom += " <button class=\"btn sai-pagination-btn\" data-offset=\"" + (jso.offset + 6) + "\">Older &gt;</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;
Page fetched 0s ago, creation time: 6ms (vhost etag hits: 0%, cache hits: 0%)