diff --git a/assets/index.html b/assets/index.html
index 24776e8..5394a79 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=7"/>
+ <link rel="stylesheet" type="text/css" href="sai.css?v=9"/>
<link rel="icon" href="sai-icon.svg" sizes="any" type="image/svg+xml"/>
- <script type='text/javascript' src='sai.js?v=7'></script>
+ <script type='text/javascript' src='sai.js?v=9'></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 7b4565d..dc3345a 100644
--- a/assets/sai.css
+++ b/assets/sai.css
@@ -1891,12 +1891,18 @@ div.ibuil.vm-builder {
}
/* event rows in col 4 */
+/*
+ * Tabular event rows: CSS grid with fixed-width columns so the date/time,
+ * tag, status and progress-bar cells line up across all rows regardless of
+ * their content width (the date/time and tag are constant width; status is
+ * auto so it takes its natural size; the bar is a fixed 250px).
+ */
.sb-event-row {
- display: flex;
- flex-direction: row;
+ display: grid;
+ grid-template-columns: 150px 96px minmax(60px, auto) 250px;
align-items: center;
- gap: 6px;
- padding: 2px 6px;
+ column-gap: 10px;
+ padding: 2px 10px;
margin: 1px 0;
border-radius: 4px;
cursor: pointer;
@@ -1934,7 +1940,8 @@ div.ibuil.vm-builder {
font-size: 8pt;
color: #64748b;
white-space: nowrap;
- flex: 0 0 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.sb-event-row.comp_fail .sb-event-when {
@@ -1945,30 +1952,30 @@ div.ibuil.vm-builder {
font-family: monospace;
font-size: 9pt;
white-space: nowrap;
- flex: 0 0 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.sb-event-row .sb-event-status {
font-size: 8.5pt;
white-space: nowrap;
- flex: 0 0 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
/* progress bar slot: fixed compact width, inline on the same row */
.sb-event-row .sb-event-bar {
- flex: 0 0 250px;
- width: 250px;
- max-width: 250px;
display: inline-flex;
align-items: center;
+ min-width: 0;
}
.sb-event-row .sb-event-bar:empty {
display: none;
}
.sb-event-row .progress-bar {
- width: 250px;
- max-width: 250px;
+ width: 100%;
+ min-width: 0;
height: 8px;
}
@@ -2018,6 +2025,20 @@ div.ibuil.vm-builder {
display: inline-block;
}
+/* reflect the event's completion state on the header background, like div4 */
+.event-tasks-header.comp_pass {
+ background: #01ff70;
+ border-bottom-color: #2ecc40;
+}
+.event-tasks-header.comp_fail {
+ background: #ff4136;
+ color: #ffffff;
+ border-bottom-color: #df3030;
+}
+.event-tasks-header.comp_fail .event-tasks-title {
+ color: #ffffff;
+}
+
.tasks-table-display {
width: 100%;
border-collapse: collapse;
diff --git a/assets/sai.js b/assets/sai.js
index 685967b..bc86121 100644
--- a/assets/sai.js
+++ b/assets/sai.js
@@ -1688,6 +1688,7 @@ function selectSbProject(name)
render_sb_projects();
/* refresh the branch list for this project; auto-selects newest branch */
sai_sb_request_branches(name);
+ sai_sb_update_url();
}
function selectSbBranch(ref)
@@ -1708,6 +1709,33 @@ function selectSbBranch(ref)
}
}
render_sb_events();
+ /* reflect the project + branch selection in the URL */
+ sai_sb_update_url();
+}
+
+/*
+ * Keep the project + branch in the URL query string so the view is shareable
+ * and survives a reload. Event/task params are managed by selectEvent; we
+ * only touch project/branch here.
+ */
+function sai_sb_update_url()
+{
+ try {
+ var par = new URLSearchParams(window.location.search);
+ if (sb_selected_project)
+ par.set("project", sb_selected_project);
+ else
+ par.delete("project");
+ if (sb_selected_ref)
+ par.set("branch", sb_selected_ref);
+ else
+ par.delete("branch");
+ var qs = par.toString();
+ var path = window.location.pathname;
+ if (!path.endsWith('/') && !path.endsWith('index.html'))
+ path += '/';
+ window.history.replaceState({}, "", path + (qs ? ("?" + qs) : ""));
+ } catch (e) {}
}
function render_event_decals() {
@@ -1749,7 +1777,10 @@ function render_selected_event_tasks(o) {
* available yet (sidebar-scoped overview events arrive with t:[] and
* fetch their tasks on demand via selectEvent -> eventinfo).
*/
- s += "<div class=\"event-tasks-header\">";
+ s += "<div class=\"event-tasks-header";
+ if (e.state == 3) s += " comp_pass";
+ if (e.state == 4 || e.state == 6) s += " comp_fail";
+ s += "\">";
var refName = e.ref.replace("refs/heads/", "").replace("refs/tags/", "");
s += "<span class=\"event-tasks-title\">" + san(e.repo_name) + " (" + san(refName) + ") - " + san(sai_event_hash_display(e.hash)) + "</span>";
/* admin-only restart-all / delete-event controls live here now */
@@ -2870,7 +2901,17 @@ function ws_open_sai()
* the project list. Its handler auto-selects the first
* project -> branchlist -> auto-select newest branch ->
* scoped overview.
+ *
+ * If the URL carries ?project= / ?branch= (a previously
+ * saved sidebar selection), pre-seed them so the cascade
+ * restores that view instead of the newest project/branch.
*/
+ {
+ var _pp = par.get('project');
+ var _bp = par.get('branch');
+ if (_pp) sb_selected_project = _pp;
+ if (_bp) sb_selected_ref = _bp;
+ }
sai_sb_request_projects();
};
@@ -3139,23 +3180,41 @@ function ws_open_sai()
*/
sb_projects = (jso.projects && Array.isArray(jso.projects)) ? jso.projects : [];
if (!sb_selected_project && !selected_event_uuid &&
- !selected_task_uuid && sb_projects.length)
+ !selected_task_uuid && sb_projects.length) {
selectSbProject(sb_projects[0]);
- else
+ } else {
render_sb_projects();
+ /*
+ * If a project was pre-selected (e.g. from ?project=
+ * in the URL), drive the rest of the cascade for it:
+ * fetch its branch list. When a branch is also
+ * pre-selected, the branchlist handler below will
+ * preserve it and fire the scoped overview.
+ */
+ if (sb_selected_project && !sb_branches.length)
+ sai_sb_request_branches(sb_selected_project);
+ }
break;
case "com.warmcat.sai.branchlist":
/*
* Unique refs for the selected project,
* newest-first. Auto-select the most recent
- * branch so col 4 populates immediately.
- */
+ * branch so col 4 populates immediately.
+ */
sb_branches = (jso.branches && Array.isArray(jso.branches)) ? jso.branches : [];
- if (!sb_selected_ref && sb_branches.length)
+ if (!sb_selected_ref && sb_branches.length) {
selectSbBranch(sb_branches[0]);
- else
+ } else {
render_sb_branches();
+ /*
+ * If a branch was pre-selected (e.g. from ?branch=),
+ * fire the scoped overview now that we know the
+ * branch list for this project.
+ */
+ if (sb_selected_ref)
+ sai_sb_request_overview(0);
+ }
break;
case "sai.warmcat.com.overview":
@@ -3219,6 +3278,35 @@ function ws_open_sai()
selected_event_uuid = loaded_events[loaded_events.length - 1].e.uuid;
}
}
+
+ /*
+ * Deep-link (?event= / ?task=) support: if the
+ * URL pinned a specific event/task, derive the
+ * sidebar project + branch from it so the
+ * project and branch lists highlight the right
+ * entries. We only do this once (until the
+ * user manually picks something else).
+ */
+ var _deep = false;
+ try {
+ var _dp = new URLSearchParams(window.location.search);
+ _deep = !!(_dp.get('event') || _dp.get('task'));
+ } catch (e2) {}
+ if (_deep && selected_event_uuid &&
+ !sb_selected_project && !sb_selected_ref) {
+ var _dev = loaded_events.find(
+ o => o.e.uuid === selected_event_uuid);
+ if (_dev && _dev.e) {
+ sb_selected_project = _dev.e.repo_name || null;
+ sb_selected_ref = _dev.e.ref || null;
+ render_sb_projects();
+ render_sb_branches();
+ /* refresh this project's branch list from
+ * the server so the highlight is correct */
+ if (sb_selected_project)
+ sai_sb_request_branches(sb_selected_project);
+ }
+ }
}
/*
@@ -4096,6 +4184,19 @@ window.addEventListener("load", function() {
container.classList.add('grant-none');
}
}
+
+ /*
+ * Re-render the tasks pane header so the admin
+ * restart-all / delete-event buttons appear now that
+ * auth_state has resolved (the buttons are gated on
+ * LOGGED_IN_GRANT_ADMIN and are absent from the initial
+ * render while auth_state was still NOT_LOGGED_IN).
+ */
+ if (selected_event_uuid) {
+ var _ev = loaded_events.find(o => o.e.uuid === selected_event_uuid);
+ if (_ev)
+ render_selected_event_tasks(_ev);
+ }
}
})
.catch(function(err) {