Compare commits

..
5 changed files with 18 additions and 27 deletions

View file

@ -376,17 +376,12 @@ function renderQueueEntry(entry) {
chip.append(el('span', { class: 'rqe-node-agent' }, ' · ' + n.agent)); chip.append(el('span', { class: 'rqe-node-agent' }, ' · ' + n.agent));
} }
row.append(chip); row.append(chip);
if (n.build_log_id != null) { if (n.has_log) {
// Deep-links into the BUILD L0GS tab's rich view (auto-expands +
// scrolls to this row there, see fetchBuild's `?id=N` handling)
// rather than the raw-text download endpoint — a plain download
// is surprising here since nothing about a printer-glyph icon
// says "this leaves the app". The raw download is still one
// click away once on that row.
row.append(el('a', { row.append(el('a', {
class: 'rqe-log-link rqe-node-log', class: 'rqe-log-link rqe-node-log',
href: '/builds.html?id=' + n.build_log_id + '#buildlogs', href: '/api/build-log/' + n.id + '/raw',
title: 'view build log for ' + n.kind + ' node', target: '_blank',
title: 'download build log for ' + n.kind + ' node',
}, '⎙')); }, '⎙'));
} }
treeRoot.append(row); treeRoot.append(row);
@ -444,11 +439,11 @@ function clearLiveLogPoll() {
} }
// First (entry, node) pair with a running node that has a log. // First (entry, node) pair with a running node that has a log.
// Gate on build_log_id so lock/noop/store-only nodes don't open a blank panel. // Gate on has_log so lock/noop/store-only nodes don't open a blank panel.
function findLiveBuild(queue) { function findLiveBuild(queue) {
for (const e of queue || []) { for (const e of queue || []) {
if (rollupState(e.nodes || []) !== 'Running') continue; if (rollupState(e.nodes || []) !== 'Running') continue;
const node = (e.nodes || []).find((n) => n.state === 'Running' && n.build_log_id != null); const node = (e.nodes || []).find((n) => n.state === 'Running' && n.has_log);
if (node) return { entry: e, node }; if (node) return { entry: e, node };
} }
return null; return null;

View file

@ -130,11 +130,9 @@ pub(super) async fn get_build_log_full(
/// `GET /api/build-log/{node_id}` — the build log for a **queue node**, /// `GET /api/build-log/{node_id}` — the build log for a **queue node**,
/// resolved node id → log-row id → full log. Same `BuildLogFull` JSON /// resolved node id → log-row id → full log. Same `BuildLogFull` JSON
/// (`stdout` / `stderr` + header) as `get_build_log_full`; HTTP 404 when the /// (`stdout` / `stderr` + header) as `get_build_log_full`; HTTP 404 when the
/// node has no linked log (the client gates the request on /// node has no linked log (the client gates the request on `NodeView.has_log`,
/// `NodeView.build_log_id`, but a vacuum race can still 404). This is the /// but a vacuum race can still 404). This is the on-demand log fetch the
/// on-demand live-log-panel fetch, distinct from the `build_log_id` on the /// raw-graph dashboard uses instead of an inline `build_log_id` on the wire.
/// wire — that id is for deep-linking to the BUILD L0GS tab's full history
/// view, not for fetching the log content itself.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/build-log/{node_id}", path = "/api/build-log/{node_id}",

View file

@ -559,7 +559,7 @@ impl QueueInner {
} else { } else {
Vec::new() Vec::new()
}; };
let build_log_id = self.node_rt.get(&id).and_then(|r| r.build_log_id); let has_log = self.node_rt.get(&id).and_then(|r| r.build_log_id).is_some();
// `node.parent` is the structural jobq parent. Top-level nodes // `node.parent` is the structural jobq parent. Top-level nodes
// have `parent == Some(container)` (direct children of the Dag // have `parent == Some(container)` (direct children of the Dag
// container); those become `parent: None` on the wire since the // container); those become `parent: None` on the wire since the
@ -580,7 +580,7 @@ impl QueueInner {
error: node.error.clone(), error: node.error.clone(),
approval_id, approval_id,
inputs, inputs,
build_log_id, has_log,
parent, parent,
}); });
} }

View file

@ -105,14 +105,12 @@ pub struct NodeView {
/// node. Display-only payload, not derivable from the graph. /// node. Display-only payload, not derivable from the graph.
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<String>, pub inputs: Vec<String>,
/// The build-log history row id, when this node has a captured build /// Whether this node has a captured build log fetchable at
/// log fetchable at `GET /api/build-log/<node id>` and deep-linkable to /// `GET /api/build-log/<id>`. Only the nix-heavy nodes that stream build
/// `/builds.html?id=<this>#buildlogs`. Only the nix-heavy nodes that /// output set one; the client gates its log link on this so lock / noop /
/// stream build output set one; the client gates its log link on /// store-only nodes don't render a link that 404s.
/// `.is_some()` so lock / noop / store-only nodes don't render a link #[serde(default)]
/// that 404s. pub has_log: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_log_id: Option<i64>,
/// Structural parent in the jobq tree — `None` for top-level nodes /// Structural parent in the jobq tree — `None` for top-level nodes
/// (direct children of the DAG container). Sub-nodes carry the id of /// (direct children of the DAG container). Sub-nodes carry the id of
/// their containing parent node. The client uses this to render the /// their containing parent node. The client uses this to render the

View file

@ -342,7 +342,7 @@ mod tests {
error: None, error: None,
approval_id: None, approval_id: None,
inputs: vec![], inputs: vec![],
build_log_id: None, has_log: false,
} }
} }