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-08-08 15:33 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-08-08 15:33 UTC
Treea014845603b09fd7e07f3edb676d62ef2e95cac5   Raw Patch
 
rejig-fixes
rejig-fixes
diff --git a/assets/index.html b/assets/index.html index ed488bc..45b540b 100644 --- a/assets/index.html +++ b/assets/index.html @@ -2,9 +2,9 @@ <html lang="en"> <head> <meta charset=utf-8 http-equiv="Content-Language" content="en"/> - <link rel="stylesheet" type="text/css" href="sai.css?v=10"/> + <link rel="stylesheet" type="text/css" href="sai.css?v=11"/> <link rel="icon" href="sai-icon.svg" sizes="any" type="image/svg+xml"/> - <script type='text/javascript' src='sai.js?v=10'></script> + <script type='text/javascript' src='sai.js?v=11'></script> <script type='text/javascript' src='terminal-core.js'></script> <script type='text/javascript' src='sai-terminal.js'></script> <script type='text/javascript' src='lws-login.js'></script> diff --git a/assets/sai.css b/assets/sai.css index dc3345a..f274d74 100644 --- a/assets/sai.css +++ b/assets/sai.css @@ -1773,6 +1773,20 @@ div.ibuil.vm-builder { overflow: hidden; } +/* + * Builder list pane: a flex child of .right-pane. Takes the remaining height + * beneath the power overview and scrolls independently when the builder list is + * taller than the viewport, so clipped builders stay reachable. min-height:0 + * is required so the flex item can shrink below its content size and actually + * scroll instead of forcing the pane to grow. + */ +#sai_builders { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + scrollbar-width: thin; +} + .sidebar-pane { flex: 0 0 auto; height: 150px; @@ -1890,6 +1904,26 @@ div.ibuil.vm-builder { font-weight: bold; } +/* + * Branch rows are coloured by the state of their newest event: green for a + * successful build, red for a failed one (states 3 / 4 / 6, matching the + * event-row semantics). Ongoing / waiting states keep the default styling. + * Brighter hues are used on the dark selected-row background so the colour + * stays readable whether or not the branch is the current selection. + */ +.sb-row.comp_pass { + color: #1b8f3a; +} +.sb-row.comp_fail { + color: #c3261d; +} +.sb-row.selected.comp_pass { + color: #2ecc40; +} +.sb-row.selected.comp_fail { + color: #ff6b61; +} + /* event rows in col 4 */ /* * Tabular event rows: CSS grid with fixed-width columns so the date/time, diff --git a/assets/sai.js b/assets/sai.js index b27831b..0cad16d 100644 --- a/assets/sai.js +++ b/assets/sai.js @@ -956,6 +956,13 @@ var loaded_events = [], selected_event_uuid = null, selected_task_uuid = null, t * sb_projects / sb_branches are the unique values advertised by the server. */ var sb_projects = [], sb_branches = []; +/* + * Latest non-deleted event state per ref for the selected project, as reported + * by the branchlist reply (ref -> state int). Used to colour branch rows by + * the build result of their newest event. Empty when no project is selected + * or when an older server omits branch_states. + */ +var sb_branch_states = {}; var sb_selected_project = null, sb_selected_ref = null; /* @@ -1578,6 +1585,52 @@ function render_sb_projects() }); } +/* + * Map a latest-event state to the same comp_pass / comp_fail class family the + * event rows use, so a branch name is coloured by its newest build result. + * Returns "" for ongoing / waiting states so they keep the default styling. + */ +function sai_sb_branch_state_class(ref) +{ + if (!sb_branch_states) + return ""; + var st = sb_branch_states[ref]; + if (st === 3) + return " comp_pass"; + if (st === 4 || st === 6) + return " comp_fail"; + return ""; +} + +/* + * Live-update the selected branch's colour: the server only pushes scoped + * events for the current selection, so only sb_selected_ref can have moved. + * Recompute the newest matching event's state from loaded_events; if it differs + * from the recorded sb_branch_states entry, update it and re-render col 3. + */ +function sai_sb_branch_state_livecheck() +{ + if (!sb_selected_ref || !sb_branch_states || + !(sb_selected_ref in sb_branch_states) || + !loaded_events || !loaded_events.length) + return; + var ml = loaded_events.filter(function(o) { + return o && o.e && o.e.ref === sb_selected_ref && + (!sb_selected_project || + o.e.repo_name === sb_selected_project); + }); + if (!ml.length) + return; + ml.sort(function(a, b) { + return (b.e.created || 0) - (a.e.created || 0); + }); + var nst = ml[0].e.state; + if (sb_branch_states[sb_selected_ref] !== nst) { + sb_branch_states[sb_selected_ref] = nst; + render_sb_branches(); + } +} + function render_sb_branches() { var c = document.getElementById("sai_sb_branches"); @@ -1589,8 +1642,10 @@ function render_sb_branches() } else { sb_branches.forEach(function(ref) { var sel = (ref === sb_selected_ref) ? " selected" : ""; - s += "<div class=\"sb-row" + sel + "\" data-ref=\"" + - san(ref) + "\">" + san(sai_sb_short_ref(ref)) + "</div>"; + var stClass = sai_sb_branch_state_class(ref); + s += "<div class=\"sb-row" + stClass + sel + + "\" data-ref=\"" + san(ref) + "\">" + + san(sai_sb_short_ref(ref)) + "</div>"; }); } c.innerHTML = s; @@ -1684,6 +1739,8 @@ function selectSbProject(name) return; sb_selected_project = name; sb_selected_ref = null; + /* clear the per-ref state map until the new project's branchlist arrives */ + sb_branch_states = {}; render_sb_projects(); /* refresh the branch list for this project; auto-selects newest branch */ sai_sb_request_branches(name); @@ -3212,8 +3269,12 @@ function ws_open_sai() * Unique refs for the selected project, * newest-first. Auto-select the most recent * branch so col 4 populates immediately. + * branch_states (ref -> latest-event state) is + * optional; older servers omit it. */ sb_branches = (jso.branches && Array.isArray(jso.branches)) ? jso.branches : []; + sb_branch_states = (jso.branch_states && typeof jso.branch_states === 'object') + ? jso.branch_states : {}; if (!sb_selected_ref && sb_branches.length) { selectSbBranch(sb_branches[0]); } else { @@ -3382,6 +3443,26 @@ function ws_open_sai() sb_branches = Object.keys(_bset).sort(function(a, b) { return _bset[b] - _bset[a]; }); + /* + * Derive the per-ref latest state client-side too, + * using the newest event per ref, so the fallback + * path colours branches identically to a real + * branchlist reply. + */ + sb_branch_states = {}; + sb_branches.forEach(function(ref) { + var newest = null; + loaded_events.forEach(function(o) { + if (!o || !o.e || o.e.ref !== ref || + o.e.repo_name !== sb_selected_project) + return; + if (!newest || + (o.e.created || 0) > (newest.e.created || 0)) + newest = o; + }); + if (newest) + sb_branch_states[ref] = newest.e.state; + }); if (!sb_selected_ref && sb_branches.length) sb_selected_ref = sb_branches[0]; render_sb_branches(); @@ -3428,6 +3509,14 @@ function ws_open_sai() } } + /* + * Keep the selected branch's colour live: the + * overview is scoped to the selection, so only the + * selected ref can have a newer event here; other + * branches refresh on the next branchlist fetch. + */ + sai_sb_branch_state_livecheck(); + aging(); break; @@ -3457,6 +3546,12 @@ function ws_open_sai() } } } + /* + * A task state change can flip the event's + * state; reflect it in the selected branch's + * colour without a full branchlist round-trip. + */ + sai_sb_branch_state_livecheck(); } if (document.getElementById("taskstate_" + jso.t.uuid)) { diff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c index f0dbed5..e6ca32b 100644 --- a/src/web/w-ws-browser.c +++ b/src/web/w-ws-browser.c @@ -847,45 +847,99 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, /* * Return the set of unique refs for the given project, in * most-recent-first order (the ref whose latest event is the - * newest comes first). Read-only. project is bound, not + * newest comes first). A parallel "branch_states" object maps + * each ref to the state of its latest non-deleted event, so the + * browser can colour the branch list rows by build result. + * Read-only. project and the state filters are bound, not * interpolated. + * + * We run the grouped query once into an lwsac snapshot, then + * emit both the "branches" array and the "branch_states" object + * from that single consistent result. The correlated subquery + * resolves the state of the newest non-deleted event for the + * group's ref within the same project. Bind order follows + * parameter appearance: project, SAIES_DELETED (subquery), + * then SAIES_DELETED, project (outer). */ sai_browse_rx_branchlist_t *bl = (sai_browse_rx_branchlist_t *)a.dest; + struct bl_row { + struct lws_dll2 list; + char *ref; + int state; + }; + struct lwsac *ac = NULL; + lws_dll2_owner_t owner; sqlite3_stmt *stmt = NULL; uint8_t buf[LWS_PRE + 4096], *start = buf + LWS_PRE, *p = start, *end = buf + sizeof(buf); char esc[96], pesc[96]; int first_elem = 1, sent_any = 0, rc; + lws_dll2_owner_init(&owner); + + /* + * Belt-and-braces: also purify (the bound param already + * prevents injection, but this keeps the echoed project + * field safe to emit too). + */ + lws_sql_purify(pesc, bl->project, sizeof(pesc) - 1); + if (sqlite3_prepare_v2(vhd->pdb, - "SELECT ref, MAX(created) AS mc FROM events " + "SELECT ref, " + "(SELECT e2.state FROM events e2 " + " WHERE e2.ref = events.ref " + " AND e2.repo_name = ? AND e2.state != ? " + " ORDER BY e2.created DESC LIMIT 1) AS ls " + "FROM events " "WHERE state != ? AND repo_name = ? " - "GROUP BY ref ORDER BY mc DESC", + "GROUP BY ref ORDER BY MAX(created) DESC", -1, &stmt, NULL) != SQLITE_OK) { lwsl_notice("%s: branchlist prepare failed\n", __func__); goto soft_error; } - sqlite3_bind_int(stmt, 1, SAIES_DELETED); - /* - * Belt-and-braces: also purify (the bound param already - * prevents injection, but this keeps the echoed project - * field safe to emit too). - */ - lws_sql_purify(pesc, bl->project, sizeof(pesc) - 1); - sqlite3_bind_text(stmt, 2, pesc, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 1, pesc, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, SAIES_DELETED); + sqlite3_bind_int(stmt, 3, SAIES_DELETED); + sqlite3_bind_text(stmt, 4, pesc, -1, SQLITE_STATIC); - p += lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), - "{\"schema\":\"com.warmcat.sai.branchlist\"," - "\"project\":\"%s\",\"branches\":[", - lws_json_purify(esc, pesc, - sizeof(esc) - 1, NULL)); while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + struct bl_row *row; const char *rn = (const char *) sqlite3_column_text(stmt, 0); + size_t rn_len; if (!rn) continue; + rn_len = strlen(rn); + row = lwsac_use_zero(&ac, sizeof(*row), 0); + if (!row) { + sqlite3_finalize(stmt); + lwsac_free(&ac); + goto soft_error; + } + row->ref = lwsac_use(&ac, rn_len + 1, 0); + if (!row->ref) { + sqlite3_finalize(stmt); + lwsac_free(&ac); + goto soft_error; + } + memcpy(row->ref, rn, rn_len + 1); + row->state = sqlite3_column_int(stmt, 1); + lws_dll2_add_tail(&row->list, &owner); + } + sqlite3_finalize(stmt); + + p += lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), + "{\"schema\":\"com.warmcat.sai.branchlist\"," + "\"project\":\"%s\",\"branches\":[", + lws_json_purify(esc, pesc, + sizeof(esc) - 1, NULL)); + first_elem = 1; + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, + owner.head) { + struct bl_row *row = lws_container_of(d, + struct bl_row, list); if (lws_ptr_diff_size_t(end, p) < 96) { saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, start, lws_ptr_diff_size_t(p, start), @@ -897,13 +951,44 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, p += lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), "%s\"%s\"", first_elem ? "" : ",", - lws_json_purify(esc, rn, sizeof(esc) - 1, NULL)); + lws_json_purify(esc, row->ref, + sizeof(esc) - 1, NULL)); first_elem = 0; - } - sqlite3_finalize(stmt); + } lws_end_foreach_dll_safe(d, d1); + /* + * Parallel ref -> latest-state map. Kept as a separate object + * so the existing "branches" string array stays unchanged for + * older clients that ignore the extra field. + */ p += lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), - "]}"); + "],\"branch_states\":{"); + first_elem = 1; + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, + owner.head) { + struct bl_row *row = lws_container_of(d, + struct bl_row, list); + if (lws_ptr_diff_size_t(end, p) < 96) { + saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, + start, lws_ptr_diff_size_t(p, start), + lws_write_ws_flags(LWS_WRITE_TEXT, + !sent_any, 0)); + sent_any = 1; + p = start; + } + p += lws_snprintf((char *)p, + lws_ptr_diff_size_t(end, p), "%s\"%s\":%d", + first_elem ? "" : ",", + lws_json_purify(esc, row->ref, + sizeof(esc) - 1, NULL), + row->state); + first_elem = 0; + } lws_end_foreach_dll_safe(d, d1); + + p += lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), + "}}"); + lwsac_free(&ac); + saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, start, lws_ptr_diff_size_t(p, start), LWS_WRITE_TEXT);
Page fetched 0s ago, creation time: 8ms (vhost etag hits: 0%, cache hits: 0%)