From 0697a2facd63908b45495fa0a1e94c982e34f052 Mon Sep 17 00:00:00 2001 From: zousar Date: Sat, 14 Feb 2026 23:51:54 -0700 Subject: Enhance dependencies to include soft and hard deps --- src/zenserver/frontend/html/pages/entry.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/pages/entry.js b/src/zenserver/frontend/html/pages/entry.js index 08589b090..212686e42 100644 --- a/src/zenserver/frontend/html/pages/entry.js +++ b/src/zenserver/frontend/html/pages/entry.js @@ -155,7 +155,7 @@ export class Page extends ZenPage if (Object.keys(tree).length != 0) { - const sub_section = section.add_section("deps"); + const sub_section = section.add_section("dependencies"); this._build_deps(sub_section, tree); } } @@ -271,16 +271,18 @@ export class Page extends ZenPage for (const field of pkgst_entry) { const field_name = field.get_name(); - if (!field_name.endsWith("importedpackageids")) - continue; - - var dep_name = field_name.slice(0, -18); - if (dep_name.length == 0) - dep_name = "imported"; - - var out = tree[dep_name] = []; - for (var item of field.as_array()) - out.push(item.as_value(BigInt)); + if (field_name == "importedpackageids") + { + var out = tree["hard"] = []; + for (var item of field.as_array()) + out.push(item.as_value(BigInt)); + } + else if (field_name == "softpackagereferences") + { + var out = tree["soft"] = []; + for (var item of field.as_array()) + out.push(item.as_value(BigInt)); + } } return tree; -- cgit v1.2.3 From c40e2c7625cf6aab25862c1c18caeb8577884656 Mon Sep 17 00:00:00 2001 From: zousar Date: Sun, 15 Feb 2026 11:55:17 -0700 Subject: Restore handling for hard/soft name prefixes --- src/zenserver/frontend/html/pages/entry.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/pages/entry.js b/src/zenserver/frontend/html/pages/entry.js index 212686e42..76afd3e1f 100644 --- a/src/zenserver/frontend/html/pages/entry.js +++ b/src/zenserver/frontend/html/pages/entry.js @@ -271,15 +271,27 @@ export class Page extends ZenPage for (const field of pkgst_entry) { const field_name = field.get_name(); - if (field_name == "importedpackageids") + if (field_name.endsWith("importedpackageids")) { - var out = tree["hard"] = []; + var dep_name = field_name.slice(0, -18); + if (dep_name.length == 0) + dep_name = "hard"; + else + dep_name = "hard." + dep_name; + + var out = tree[dep_name] = []; for (var item of field.as_array()) out.push(item.as_value(BigInt)); } - else if (field_name == "softpackagereferences") + else if (field_name.endsWith("softpackagereferences")) { - var out = tree["soft"] = []; + var dep_name = field_name.slice(0, -21); + if (dep_name.length == 0) + dep_name = "soft"; + else + dep_name = "soft." + dep_name; + + var out = tree[dep_name] = []; for (var item of field.as_array()) out.push(item.as_value(BigInt)); } -- cgit v1.2.3 From 81a6d5e29453db761d058b6418044c8cf04a167e Mon Sep 17 00:00:00 2001 From: zousar Date: Sun, 15 Feb 2026 23:44:17 -0700 Subject: Add support for listing files on oplog entries --- src/zenserver/frontend/html/pages/entry.js | 119 ++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 9 deletions(-) (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/pages/entry.js b/src/zenserver/frontend/html/pages/entry.js index 76afd3e1f..26ea78142 100644 --- a/src/zenserver/frontend/html/pages/entry.js +++ b/src/zenserver/frontend/html/pages/entry.js @@ -76,6 +76,21 @@ export class Page extends ZenPage return null; } + _is_null_io_hash_string(io_hash) + { + if (!io_hash) + return true; + + for (let char of io_hash) + { + if (char != '0') + { + return false; + } + } + return true; + } + async _build_meta(section, entry) { var tree = {} @@ -142,30 +157,34 @@ export class Page extends ZenPage const name = entry.find("key").as_value(); var section = this.add_section(name); + var has_package_data = false; // tree { var tree = entry.find("$tree"); if (tree == undefined) tree = this._convert_legacy_to_tree(entry); - if (tree == undefined) - return this._display_unsupported(section, entry); - - delete tree["$id"]; - - if (Object.keys(tree).length != 0) + if (tree != undefined) { - const sub_section = section.add_section("dependencies"); - this._build_deps(sub_section, tree); + delete tree["$id"]; + + if (Object.keys(tree).length != 0) + { + const sub_section = section.add_section("dependencies"); + this._build_deps(sub_section, tree); + } + has_package_data = true; } } // meta + if (has_package_data) { this._build_meta(section, entry); } // data + if (has_package_data) { const sub_section = section.add_section("data"); const table = sub_section.add_widget( @@ -181,7 +200,7 @@ export class Page extends ZenPage for (const item of pkg_data.as_array()) { - var io_hash, size, raw_size, file_name; + var io_hash = undefined, size = undefined, raw_size = undefined, file_name = undefined; for (const field of item.as_object()) { if (field.is_named("data")) io_hash = field.as_value(); @@ -219,12 +238,94 @@ export class Page extends ZenPage } } + // files + var has_file_data = false; + { + const sub_section = section.add_section("files"); + const table = sub_section.add_widget( + Table, + ["name", "actions"], Table.Flag_PackRight + ); + table.id("filetable"); + for (const field_name of ["files"]) + { + var file_data = entry.find(field_name); + if (file_data == undefined) + continue; + + has_file_data = true; + + for (const item of file_data.as_array()) + { + var io_hash = undefined, cid = undefined, server_path = undefined, client_path = undefined; + for (const field of item.as_object()) + { + if (field.is_named("data")) io_hash = field.as_value(); + else if (field.is_named("id")) cid = field.as_value(); + else if (field.is_named("serverpath")) server_path = field.as_value(); + else if (field.is_named("clientpath")) client_path = field.as_value(); + } + + if (io_hash instanceof Uint8Array) + { + var ret = ""; + for (var x of io_hash) + ret += x.toString(16).padStart(2, "0"); + io_hash = ret; + } + + if (cid instanceof Uint8Array) + { + var ret = ""; + for (var x of cid) + ret += x.toString(16).padStart(2, "0"); + cid = ret; + } + + const row = table.add_row(server_path); + + var base_name = server_path.split("/").pop().split("\\").pop(); + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + if (this._is_null_io_hash_string(io_hash)) + { + const link = row.get_cell(0).link( + "/" + ["prj", project, "oplog", oplog, cid].join("/") + ); + link.first_child().attr("download", `${cid}_${base_name}`); + + const action_tb = new Toolbar(row.get_cell(-1), true); + action_tb.left().add("copy-id").on_click(async (v) => { + await navigator.clipboard.writeText(v); + }, cid); + } + else + { + const link = row.get_cell(0).link( + "/" + ["prj", project, "oplog", oplog, io_hash].join("/") + ); + link.first_child().attr("download", `${io_hash}_${base_name}`); + + const action_tb = new Toolbar(row.get_cell(-1), true); + action_tb.left().add("copy-hash").on_click(async (v) => { + await navigator.clipboard.writeText(v); + }, io_hash); + } + + } + } + } + // props + if (has_package_data) { const object = entry.to_js_object(); var sub_section = section.add_section("props"); sub_section.add_widget(PropTable).add_object(object); } + + if (!has_package_data && !has_file_data) + return this._display_unsupported(section, entry); } _display_unsupported(section, entry) -- cgit v1.2.3 From df806dcb92f0b5c9622586460fc86e698ca03ab6 Mon Sep 17 00:00:00 2001 From: zousar Date: Sun, 15 Feb 2026 23:44:54 -0700 Subject: Change breadcrumbs for oplogs to be more descriptive --- src/zenserver/frontend/html/pages/oplog.js | 2 +- src/zenserver/frontend/html/pages/page.js | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/pages/oplog.js b/src/zenserver/frontend/html/pages/oplog.js index 879fc4c97..a286f8651 100644 --- a/src/zenserver/frontend/html/pages/oplog.js +++ b/src/zenserver/frontend/html/pages/oplog.js @@ -32,7 +32,7 @@ export class Page extends ZenPage this.set_title("oplog - " + oplog); - var section = this.add_section(project + " - " + oplog); + var section = this.add_section(oplog); oplog_info = await oplog_info; this._index_max = oplog_info["opcount"]; diff --git a/src/zenserver/frontend/html/pages/page.js b/src/zenserver/frontend/html/pages/page.js index 9a9541904..2f9643008 100644 --- a/src/zenserver/frontend/html/pages/page.js +++ b/src/zenserver/frontend/html/pages/page.js @@ -97,7 +97,7 @@ export class ZenPage extends PageBase generate_crumbs() { - const auto_name = this.get_param("page") || "start"; + var auto_name = this.get_param("page") || "start"; if (auto_name == "start") return; @@ -114,15 +114,21 @@ export class ZenPage extends PageBase var project = this.get_param("project"); if (project != undefined) { + auto_name = project; var oplog = this.get_param("oplog"); if (oplog != undefined) { - new_crumb("project", `?page=project&project=${project}`); - if (this.get_param("opkey")) - new_crumb("oplog", `?page=oplog&project=${project}&oplog=${oplog}`); + auto_name = oplog; + new_crumb(project, `?page=project&project=${project}`); + var opkey = this.get_param("opkey") + if (opkey != undefined) + { + auto_name = opkey.split("/").pop().split("\\").pop();; + new_crumb(oplog, `?page=oplog&project=${project}&oplog=${oplog}`); + } } } - new_crumb(auto_name.toLowerCase()); + new_crumb(auto_name); } } -- cgit v1.2.3 From ccfcb14ef1b837ed6f752ae4f27e0ef88a5b18da Mon Sep 17 00:00:00 2001 From: zousar Date: Mon, 16 Feb 2026 16:39:44 -0700 Subject: Added custom page for cook.artifacts --- src/zenserver/frontend/html/pages/cookartifacts.js | 385 +++++++++++++++++++++ src/zenserver/frontend/html/pages/entry.js | 14 +- src/zenserver/frontend/html/pages/page.js | 15 +- src/zenserver/frontend/html/zen.css | 18 + 4 files changed, 428 insertions(+), 4 deletions(-) create mode 100644 src/zenserver/frontend/html/pages/cookartifacts.js (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/pages/cookartifacts.js b/src/zenserver/frontend/html/pages/cookartifacts.js new file mode 100644 index 000000000..6c36c7f32 --- /dev/null +++ b/src/zenserver/frontend/html/pages/cookartifacts.js @@ -0,0 +1,385 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { ZenPage } from "./page.js" +import { Fetcher } from "../util/fetcher.js" +import { Table, Toolbar, PropTable } from "../util/widgets.js" + +//////////////////////////////////////////////////////////////////////////////// +export class Page extends ZenPage +{ + main() + { + this.set_title("cook artifacts"); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + const opkey = this.get_param("opkey"); + const artifact_hash = this.get_param("hash"); + + // Fetch the artifact content as JSON + this._artifact = new Fetcher() + .resource("prj", project, "oplog", oplog, artifact_hash + ".json") + .json(); + + // Optionally fetch entry info for display context + if (opkey) + { + this._entry = new Fetcher() + .resource("prj", project, "oplog", oplog, "entries") + .param("opkey", opkey) + .cbo(); + } + + this._build_page(); + } + + // Map CookDependency enum values to display names + _get_dependency_type_name(type_value) + { + const type_names = { + 0: "None", + 1: "File", + 2: "Function", + 3: "TransitiveBuild", + 4: "Package", + 5: "ConsoleVariable", + 6: "Config", + 7: "SettingsObject", + 8: "NativeClass", + 9: "AssetRegistryQuery", + 10: "RedirectionTarget" + }; + return type_names[type_value] || `Unknown (${type_value})`; + } + + // Check if Data content should be expandable + _should_make_expandable(data_string) + { + if (!data_string || data_string.length < 40) + return false; + + // Check if it's JSON array or object + if (!data_string.startsWith('[') && !data_string.startsWith('{')) + return false; + + // Check if formatting would add newlines + try { + const parsed = JSON.parse(data_string); + const formatted = JSON.stringify(parsed, null, 2); + return formatted.includes('\n'); + } catch (e) { + return false; + } + } + + // Get first line of content for collapsed state + _get_first_line(data_string) + { + if (!data_string) + return ""; + + const newline_index = data_string.indexOf('\n'); + if (newline_index === -1) + { + // No newline, truncate if too long + return data_string.length > 80 ? data_string.substring(0, 77) + "..." : data_string; + } + return data_string.substring(0, newline_index) + "..."; + } + + // Format JSON with indentation + _format_json(data_string) + { + try { + const parsed = JSON.parse(data_string); + return JSON.stringify(parsed, null, 2); + } catch (e) { + return data_string; + } + } + + // Toggle expand/collapse state + _toggle_data_cell(cell) + { + const is_expanded = cell.attr("expanded") !== null; + const full_data = cell.attr("data-full"); + + // Find the text wrapper span + const text_wrapper = cell.first_child().next_sibling(); + + if (is_expanded) + { + // Collapse: show first line only + const first_line = this._get_first_line(full_data); + text_wrapper.text(first_line); + cell.attr("expanded", null); + } + else + { + // Expand: show formatted JSON + const formatted = this._format_json(full_data); + text_wrapper.text(formatted); + cell.attr("expanded", ""); + } + } + + // Format dependency data based on its structure + _format_dependency(dep_array) + { + const type = dep_array[0]; + const formatted = {}; + + // Common patterns based on the example data: + // Type 2 (Function): [type, name, array, hash] + // Type 4 (Package): [type, path, hash] + // Type 5 (ConsoleVariable): [type, bool, array, hash] + // Type 8 (NativeClass): [type, path, hash] + // Type 9 (AssetRegistryQuery): [type, bool, object, hash] + // Type 10 (RedirectionTarget): [type, path, hash] + + if (dep_array.length > 1) + { + // Most types have a name/path as second element + if (typeof dep_array[1] === "string") + { + formatted.Name = dep_array[1]; + } + else if (typeof dep_array[1] === "boolean") + { + formatted.Value = dep_array[1].toString(); + } + } + + if (dep_array.length > 2) + { + // Third element varies + if (Array.isArray(dep_array[2])) + { + formatted.Data = JSON.stringify(dep_array[2]); + } + else if (typeof dep_array[2] === "object") + { + formatted.Data = JSON.stringify(dep_array[2]); + } + else if (typeof dep_array[2] === "string") + { + formatted.Hash = dep_array[2]; + } + } + + if (dep_array.length > 3) + { + // Fourth element is usually the hash + if (typeof dep_array[3] === "string") + { + formatted.Hash = dep_array[3]; + } + } + + return formatted; + } + + async _build_page() + { + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + const opkey = this.get_param("opkey"); + const artifact_hash = this.get_param("hash"); + + // Build page title + let title = "Cook Artifacts"; + if (this._entry) + { + try + { + const entry = await this._entry; + const entry_obj = entry.as_object().find("entry").as_object(); + const key = entry_obj.find("key").as_value(); + title = `Cook Artifacts`; + } + catch (e) + { + console.error("Failed to fetch entry:", e); + } + } + + const section = this.add_section(title); + + // Fetch and parse artifact + let artifact; + try + { + artifact = await this._artifact; + } + catch (e) + { + section.text(`Failed to load artifact: ${e.message}`); + return; + } + + // Display artifact info + const info_section = section.add_section("Artifact Info"); + const info_table = info_section.add_widget(Table, ["Property", "Value"], Table.Flag_PackRight); + + if (artifact.Version !== undefined) + info_table.add_row("Version", artifact.Version.toString()); + if (artifact.HasSaveResults !== undefined) + info_table.add_row("HasSaveResults", artifact.HasSaveResults.toString()); + if (artifact.PackageSavedHash !== undefined) + info_table.add_row("PackageSavedHash", artifact.PackageSavedHash); + + // Process SaveBuildDependencies + if (artifact.SaveBuildDependencies && artifact.SaveBuildDependencies.Dependencies) + { + this._build_dependency_section( + section, + "Save Build Dependencies", + artifact.SaveBuildDependencies.Dependencies, + artifact.SaveBuildDependencies.StoredKey + ); + } + + // Process LoadBuildDependencies + if (artifact.LoadBuildDependencies && artifact.LoadBuildDependencies.Dependencies) + { + this._build_dependency_section( + section, + "Load Build Dependencies", + artifact.LoadBuildDependencies.Dependencies, + artifact.LoadBuildDependencies.StoredKey + ); + } + + // Process RuntimeDependencies + if (artifact.RuntimeDependencies && artifact.RuntimeDependencies.length > 0) + { + const runtime_section = section.add_section("Runtime Dependencies"); + const runtime_table = runtime_section.add_widget(Table, ["Path"], Table.Flag_PackRight); + for (const dep of artifact.RuntimeDependencies) + { + const row = runtime_table.add_row(dep); + // Make Path clickable to navigate to entry + row.get_cell(0).text(dep).on_click((opkey) => { + window.location = `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey.toLowerCase()}`; + }, dep); + } + } + } + + _build_dependency_section(parent_section, title, dependencies, stored_key) + { + const section = parent_section.add_section(title); + + // Add stored key info + if (stored_key) + { + const key_toolbar = section.add_widget(Toolbar); + key_toolbar.left().add(`Key: ${stored_key}`); + } + + // Group dependencies by type + const dependencies_by_type = {}; + + for (const dep_array of dependencies) + { + if (!Array.isArray(dep_array) || dep_array.length === 0) + continue; + + const type = dep_array[0]; + if (!dependencies_by_type[type]) + dependencies_by_type[type] = []; + + dependencies_by_type[type].push(this._format_dependency(dep_array)); + } + + // Sort types numerically + const sorted_types = Object.keys(dependencies_by_type).map(Number).sort((a, b) => a - b); + + for (const type_value of sorted_types) + { + const type_name = this._get_dependency_type_name(type_value); + const deps = dependencies_by_type[type_value]; + + const type_section = section.add_section(type_name); + + // Determine columns based on available fields + const all_fields = new Set(); + for (const dep of deps) + { + for (const field in dep) + all_fields.add(field); + } + let columns = Array.from(all_fields); + + // Remove Hash column for RedirectionTarget as it's not useful + if (type_value === 10) + { + columns = columns.filter(col => col !== "Hash"); + } + + if (columns.length === 0) + { + type_section.text("No data fields"); + continue; + } + + // Create table with dynamic columns + const table = type_section.add_widget(Table, columns, Table.Flag_PackRight); + + // Check if this type should have clickable Name links + const should_link = (type_value === 3 || type_value === 4 || type_value === 10); + const name_col_index = columns.indexOf("Name"); + + for (const dep of deps) + { + const row_values = columns.map(col => dep[col] || ""); + const row = table.add_row(...row_values); + + // Make Name field clickable for Package, TransitiveBuild, and RedirectionTarget + if (should_link && name_col_index >= 0 && dep.Name) + { + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + row.get_cell(name_col_index).text(dep.Name).on_click((opkey) => { + window.location = `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey.toLowerCase()}`; + }, dep.Name); + } + + // Make Data field expandable/collapsible if needed + const data_col_index = columns.indexOf("Data"); + if (data_col_index >= 0 && dep.Data) + { + const data_cell = row.get_cell(data_col_index); + + if (this._should_make_expandable(dep.Data)) + { + // Store full data in attribute + data_cell.attr("data-full", dep.Data); + + // Clear the cell and rebuild with icon + text + data_cell.inner().innerHTML = ""; + + // Create expand/collapse icon + const icon = data_cell.tag("span").classify("zen_expand_icon").text("+"); + icon.on_click(() => { + this._toggle_data_cell(data_cell); + // Update icon text + const is_expanded = data_cell.attr("expanded") !== null; + icon.text(is_expanded ? "-" : "+"); + }); + + // Add text content wrapper + const text_wrapper = data_cell.tag("span").classify("zen_data_text"); + const first_line = this._get_first_line(dep.Data); + text_wrapper.text(first_line); + + // Store reference to text wrapper for updates + data_cell.attr("data-text-wrapper", "true"); + } + } + } + } + } +} diff --git a/src/zenserver/frontend/html/pages/entry.js b/src/zenserver/frontend/html/pages/entry.js index 26ea78142..dca3a5c25 100644 --- a/src/zenserver/frontend/html/pages/entry.js +++ b/src/zenserver/frontend/html/pages/entry.js @@ -138,11 +138,23 @@ export class Page extends ZenPage const project = this.get_param("project"); const oplog = this.get_param("oplog"); + const opkey = this.get_param("opkey"); const link = row.get_cell(0).link( - "/" + ["prj", project, "oplog", oplog, value+".json"].join("/") + (key === "cook.artifacts") ? + `?page=cookartifacts&project=${project}&oplog=${oplog}&opkey=${opkey}&hash=${value}` + : "/" + ["prj", project, "oplog", oplog, value+".json"].join("/") ); const action_tb = new Toolbar(row.get_cell(-1), true); + + // Add "view-raw" button for cook.artifacts + if (key === "cook.artifacts") + { + action_tb.left().add("view-raw").on_click(() => { + window.location = "/" + ["prj", project, "oplog", oplog, value+".json"].join("/"); + }); + } + action_tb.left().add("copy-hash").on_click(async (v) => { await navigator.clipboard.writeText(v); }, value); diff --git a/src/zenserver/frontend/html/pages/page.js b/src/zenserver/frontend/html/pages/page.js index 2f9643008..3ec0248cb 100644 --- a/src/zenserver/frontend/html/pages/page.js +++ b/src/zenserver/frontend/html/pages/page.js @@ -118,13 +118,22 @@ export class ZenPage extends PageBase var oplog = this.get_param("oplog"); if (oplog != undefined) { + new_crumb(auto_name, `?page=project&project=${project}`); auto_name = oplog; - new_crumb(project, `?page=project&project=${project}`); var opkey = this.get_param("opkey") if (opkey != undefined) { - auto_name = opkey.split("/").pop().split("\\").pop();; - new_crumb(oplog, `?page=oplog&project=${project}&oplog=${oplog}`); + new_crumb(auto_name, `?page=oplog&project=${project}&oplog=${oplog}`); + auto_name = opkey.split("/").pop().split("\\").pop(); + + // Check if we're viewing cook artifacts + var page = this.get_param("page"); + var hash = this.get_param("hash"); + if (hash != undefined && page == "cookartifacts") + { + new_crumb(auto_name, `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey}`); + auto_name = "cook artifacts"; + } } } } diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css index cc53c0519..34c265610 100644 --- a/src/zenserver/frontend/html/zen.css +++ b/src/zenserver/frontend/html/zen.css @@ -172,6 +172,24 @@ a { } } +/* expandable cell ---------------------------------------------------------- */ + +.zen_expand_icon { + cursor: pointer; + margin-right: 0.5em; + color: var(--theme_g1); + font-weight: bold; + user-select: none; +} + +.zen_expand_icon:hover { + color: var(--theme_ln); +} + +.zen_data_text { + user-select: text; +} + /* toolbar ------------------------------------------------------------------ */ .zen_toolbar { -- cgit v1.2.3 From d1324d607e54e2e97d666a2d1ece9ac9495d1eb1 Mon Sep 17 00:00:00 2001 From: zousar Date: Tue, 17 Feb 2026 20:21:26 -0700 Subject: Make files table in entry.js paginated and searchable --- src/zenserver/frontend/html/pages/entry.js | 210 +++++++++++++++++++++++------ 1 file changed, 170 insertions(+), 40 deletions(-) (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/pages/entry.js b/src/zenserver/frontend/html/pages/entry.js index dca3a5c25..13d5e44e7 100644 --- a/src/zenserver/frontend/html/pages/entry.js +++ b/src/zenserver/frontend/html/pages/entry.js @@ -26,6 +26,9 @@ export class Page extends ZenPage this._indexer = this.load_indexer(project, oplog); + this._files_index_start = Number(this.get_param("files_start", 0)) || 0; + this._files_index_count = Number(this.get_param("files_count", 50)) || 0; + this._build_page(); } @@ -253,20 +256,13 @@ export class Page extends ZenPage // files var has_file_data = false; { - const sub_section = section.add_section("files"); - const table = sub_section.add_widget( - Table, - ["name", "actions"], Table.Flag_PackRight - ); - table.id("filetable"); - for (const field_name of ["files"]) + var file_data = entry.find("files"); + if (file_data != undefined) { - var file_data = entry.find(field_name); - if (file_data == undefined) - continue; - has_file_data = true; + // Extract files into array + this._files_data = []; for (const item of file_data.as_array()) { var io_hash = undefined, cid = undefined, server_path = undefined, client_path = undefined; @@ -294,37 +290,26 @@ export class Page extends ZenPage cid = ret; } - const row = table.add_row(server_path); + this._files_data.push({ + server_path: server_path, + client_path: client_path, + io_hash: io_hash, + cid: cid + }); + } - var base_name = server_path.split("/").pop().split("\\").pop(); - const project = this.get_param("project"); - const oplog = this.get_param("oplog"); - if (this._is_null_io_hash_string(io_hash)) - { - const link = row.get_cell(0).link( - "/" + ["prj", project, "oplog", oplog, cid].join("/") - ); - link.first_child().attr("download", `${cid}_${base_name}`); - - const action_tb = new Toolbar(row.get_cell(-1), true); - action_tb.left().add("copy-id").on_click(async (v) => { - await navigator.clipboard.writeText(v); - }, cid); - } - else - { - const link = row.get_cell(0).link( - "/" + ["prj", project, "oplog", oplog, io_hash].join("/") - ); - link.first_child().attr("download", `${io_hash}_${base_name}`); - - const action_tb = new Toolbar(row.get_cell(-1), true); - action_tb.left().add("copy-hash").on_click(async (v) => { - await navigator.clipboard.writeText(v); - }, io_hash); - } + this._files_index_max = this._files_data.length; - } + const sub_section = section.add_section("files"); + this._build_files_nav(sub_section); + + this._files_table = sub_section.add_widget( + Table, + ["name", "actions"], Table.Flag_PackRight + ); + this._files_table.id("filetable"); + + this._build_files_table(this._files_index_start); } } @@ -419,4 +404,149 @@ export class Page extends ZenPage params.set("opkey", opkey); window.location.search = params; } + + _build_files_nav(section) + { + const nav = section.add_widget(Toolbar); + const left = nav.left(); + left.add("|<") .on_click(() => this._on_files_next_prev(-10e10)); + left.add("<<") .on_click(() => this._on_files_next_prev(-10)); + left.add("prev").on_click(() => this._on_files_next_prev( -1)); + left.add("next").on_click(() => this._on_files_next_prev( 1)); + left.add(">>") .on_click(() => this._on_files_next_prev( 10)); + left.add(">|") .on_click(() => this._on_files_next_prev( 10e10)); + + left.sep(); + for (var count of [10, 25, 50, 100]) + { + var handler = (n) => this._on_files_change_count(n); + left.add(count).on_click(handler, count); + } + + const right = nav.right(); + right.add(Friendly.sep(this._files_index_max)); + + right.sep(); + var search_input = right.add("search:", "label").tag("input"); + search_input.on("change", (x) => this._search_files(x.inner().value), search_input); + } + + _build_files_table(index) + { + this._files_index_count = Math.max(this._files_index_count, 1); + index = Math.min(index, this._files_index_max - this._files_index_count); + index = Math.max(index, 0); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + + const end_index = Math.min(index + this._files_index_count, this._files_index_max); + + this._files_table.clear(index); + for (var i = index; i < end_index; i++) + { + const file_item = this._files_data[i]; + const row = this._files_table.add_row(file_item.server_path); + + var base_name = file_item.server_path.split("/").pop().split("\\").pop(); + if (this._is_null_io_hash_string(file_item.io_hash)) + { + const link = row.get_cell(0).link( + "/" + ["prj", project, "oplog", oplog, file_item.cid].join("/") + ); + link.first_child().attr("download", `${file_item.cid}_${base_name}`); + + const action_tb = new Toolbar(row.get_cell(-1), true); + action_tb.left().add("copy-id").on_click(async (v) => { + await navigator.clipboard.writeText(v); + }, file_item.cid); + } + else + { + const link = row.get_cell(0).link( + "/" + ["prj", project, "oplog", oplog, file_item.io_hash].join("/") + ); + link.first_child().attr("download", `${file_item.io_hash}_${base_name}`); + + const action_tb = new Toolbar(row.get_cell(-1), true); + action_tb.left().add("copy-hash").on_click(async (v) => { + await navigator.clipboard.writeText(v); + }, file_item.io_hash); + } + } + + this.set_param("files_start", index); + this.set_param("files_count", this._files_index_count); + this._files_index_start = index; + } + + _on_files_change_count(value) + { + this._files_index_count = parseInt(value); + this._build_files_table(this._files_index_start); + } + + _on_files_next_prev(direction) + { + var index = this._files_index_start + (this._files_index_count * direction); + index = Math.max(0, index); + this._build_files_table(index); + } + + _search_files(needle) + { + if (needle.length == 0) + { + this._build_files_table(this._files_index_start); + return; + } + needle = needle.trim().toLowerCase(); + + this._files_table.clear(this._files_index_start); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + + var added = 0; + const truncate_at = this.get_param("searchmax") || 250; + for (const file_item of this._files_data) + { + if (!file_item.server_path.toLowerCase().includes(needle)) + continue; + + const row = this._files_table.add_row(file_item.server_path); + + var base_name = file_item.server_path.split("/").pop().split("\\").pop(); + if (this._is_null_io_hash_string(file_item.io_hash)) + { + const link = row.get_cell(0).link( + "/" + ["prj", project, "oplog", oplog, file_item.cid].join("/") + ); + link.first_child().attr("download", `${file_item.cid}_${base_name}`); + + const action_tb = new Toolbar(row.get_cell(-1), true); + action_tb.left().add("copy-id").on_click(async (v) => { + await navigator.clipboard.writeText(v); + }, file_item.cid); + } + else + { + const link = row.get_cell(0).link( + "/" + ["prj", project, "oplog", oplog, file_item.io_hash].join("/") + ); + link.first_child().attr("download", `${file_item.io_hash}_${base_name}`); + + const action_tb = new Toolbar(row.get_cell(-1), true); + action_tb.left().add("copy-hash").on_click(async (v) => { + await navigator.clipboard.writeText(v); + }, file_item.io_hash); + } + + if (++added >= truncate_at) + { + this._files_table.add_row("...truncated"); + break; + } + } + } } -- cgit v1.2.3 From 1c8948411e68429f613889c7e278bb0422c172a7 Mon Sep 17 00:00:00 2001 From: zousar Date: Tue, 17 Feb 2026 20:46:45 -0700 Subject: Rename the cache section in the web ui --- src/zenserver/frontend/html/pages/start.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/pages/start.js b/src/zenserver/frontend/html/pages/start.js index 4c8789431..2cf12bf12 100644 --- a/src/zenserver/frontend/html/pages/start.js +++ b/src/zenserver/frontend/html/pages/start.js @@ -46,7 +46,7 @@ export class Page extends ZenPage } // cache - var section = this.add_section("z$"); + var section = this.add_section("cache"); section.tag().classify("dropall").text("drop-all").on_click(() => this.drop_all("z$")); -- cgit v1.2.3 From fbd53c5500d4898be9e2c76646f220dd88a96f36 Mon Sep 17 00:00:00 2001 From: zousar Date: Tue, 17 Feb 2026 21:16:38 -0700 Subject: Dependencies table doesn't reflow the entries page --- src/zenserver/frontend/html/pages/entry.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/pages/entry.js b/src/zenserver/frontend/html/pages/entry.js index 13d5e44e7..c4746bf52 100644 --- a/src/zenserver/frontend/html/pages/entry.js +++ b/src/zenserver/frontend/html/pages/entry.js @@ -43,25 +43,39 @@ export class Page extends ZenPage return indexer; } - async _build_deps(section, tree) + _build_deps(section, tree) { - const indexer = await this._indexer; + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); for (const dep_name in tree) { const dep_section = section.add_section(dep_name); const table = dep_section.add_widget(Table, ["name", "id"], Table.Flag_PackRight); + for (const dep_id of tree[dep_name]) { - const cell_values = ["", dep_id.toString(16).padStart(16, "0")]; + const hex_id = dep_id.toString(16).padStart(16, "0"); + const cell_values = ["loading...", hex_id]; const row = table.add_row(...cell_values); - var opkey = indexer.lookup_id(dep_id); - row.get_cell(0).text(opkey).on_click((k) => this.view_opkey(k), opkey); + // Asynchronously resolve the name + this._resolve_dep_name(row.get_cell(0), dep_id, project, oplog); } } } + async _resolve_dep_name(cell, dep_id, project, oplog) + { + const indexer = await this._indexer; + const opkey = indexer.lookup_id(dep_id); + + if (opkey) + { + cell.text(opkey).on_click((k) => this.view_opkey(k), opkey); + } + } + _find_iohash_field(container, name) { const found_field = container.find(name); -- cgit v1.2.3 From 149a5c2faa8d59290b8b44717e504532e906aae2 Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Wed, 18 Feb 2026 11:28:03 +0100 Subject: structured compute basics (#714) this change adds the `zencompute` component, which can be used to distribute work dispatched from UE using the DDB (Derived Data Build) APIs via zenserver this change also adds a distinct zenserver compute mode (`zenserver compute`) which is intended to be used for leaf compute nodes to exercise the compute functionality without directly involving UE, a `zen exec` subcommand is also added, which can be used to feed replays through the system all new functionality is considered *experimental* and disabled by default at this time, behind the `zencompute` option in xmake config --- src/zenserver/frontend/html/compute.html | 991 +++++++++++++++++++++++++++++++ 1 file changed, 991 insertions(+) create mode 100644 src/zenserver/frontend/html/compute.html (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/compute.html b/src/zenserver/frontend/html/compute.html new file mode 100644 index 000000000..668189fe5 --- /dev/null +++ b/src/zenserver/frontend/html/compute.html @@ -0,0 +1,991 @@ + + + + + + Zen Compute Dashboard + + + + +
+
+
+

Zen Compute Dashboard

+
Last updated: Never
+
+
+
+ Checking... +
+
+ +
+ + +
Action Queue
+
+
+
Pending Actions
+
-
+
Waiting to be scheduled
+
+
+
Running Actions
+
-
+
Currently executing
+
+
+
Completed Actions
+
-
+
Results available
+
+
+ + +
+
Action Queue History
+
+ +
+
+ + +
Performance Metrics
+
+
Completion Rate
+
+
+
-
+
1 min rate
+
+
+
-
+
5 min rate
+
+
+
-
+
15 min rate
+
+
+
+
+ Total Retired + - +
+
+ Mean Rate + - +
+
+
+ + +
Workers
+
+
Worker Status
+
+ Registered Workers + - +
+ +
+ + +
Recent Actions
+
+
Action History
+
No actions recorded yet.
+ +
+ + +
System Resources
+
+
+
CPU Usage
+
-
+
Percent
+
+
+
+
+ +
+
+
+ Packages + - +
+
+ Physical Cores + - +
+
+ Logical Processors + - +
+
+
+
+
Memory
+
+ Used + - +
+
+ Total + - +
+
+
+
+
+
+
Disk
+
+ Used + - +
+
+ Total + - +
+
+
+
+
+
+
+ + + + -- cgit v1.2.3 From a948ff9570a5a9d8ec424639cba6f973247a0372 Mon Sep 17 00:00:00 2001 From: zousar Date: Wed, 18 Feb 2026 23:15:09 -0700 Subject: entry.js handles missing/native items more gracefully --- src/zenserver/frontend/html/pages/cookartifacts.js | 20 ++++++++++++++++---- src/zenserver/frontend/html/pages/entry.js | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/pages/cookartifacts.js b/src/zenserver/frontend/html/pages/cookartifacts.js index 6c36c7f32..f2ae094b9 100644 --- a/src/zenserver/frontend/html/pages/cookartifacts.js +++ b/src/zenserver/frontend/html/pages/cookartifacts.js @@ -261,13 +261,25 @@ export class Page extends ZenPage { const row = runtime_table.add_row(dep); // Make Path clickable to navigate to entry - row.get_cell(0).text(dep).on_click((opkey) => { - window.location = `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey.toLowerCase()}`; - }, dep); + if (this._should_link_dependency(dep)) + { + row.get_cell(0).text(dep).on_click((opkey) => { + window.location = `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey.toLowerCase()}`; + }, dep); + } } } } + _should_link_dependency(name) + { + // Exclude dependencies starting with /Script/ (code-defined entries) - case insensitive + if (name && name.toLowerCase().startsWith("/script/")) + return false; + + return true; + } + _build_dependency_section(parent_section, title, dependencies, stored_key) { const section = parent_section.add_section(title); @@ -338,7 +350,7 @@ export class Page extends ZenPage const row = table.add_row(...row_values); // Make Name field clickable for Package, TransitiveBuild, and RedirectionTarget - if (should_link && name_col_index >= 0 && dep.Name) + if (should_link && name_col_index >= 0 && dep.Name && this._should_link_dependency(dep.Name)) { const project = this.get_param("project"); const oplog = this.get_param("oplog"); diff --git a/src/zenserver/frontend/html/pages/entry.js b/src/zenserver/frontend/html/pages/entry.js index c4746bf52..f418b17ba 100644 --- a/src/zenserver/frontend/html/pages/entry.js +++ b/src/zenserver/frontend/html/pages/entry.js @@ -181,6 +181,22 @@ export class Page extends ZenPage async _build_page() { var entry = await this._entry; + + // Check if entry exists + if (!entry || entry.as_object().find("entry") == null) + { + const opkey = this.get_param("opkey"); + var section = this.add_section("Entry Not Found"); + section.tag("p").text(`The entry "${opkey}" is not present in this dataset.`); + section.tag("p").text("This could mean:"); + const list = section.tag("ul"); + list.tag("li").text("The entry is for an instance defined in code"); + list.tag("li").text("The entry has not been added to the oplog yet"); + list.tag("li").text("The entry key is misspelled"); + list.tag("li").text("The entry was removed or never existed"); + return; + } + entry = entry.as_object().find("entry").as_object(); const name = entry.find("key").as_value(); -- cgit v1.2.3 From cae12611580c6c28b1362fa28181b8f388516a47 Mon Sep 17 00:00:00 2001 From: zousar Date: Thu, 19 Feb 2026 13:55:44 -0700 Subject: icon and header logo changes --- src/zenserver/frontend/html/epicgames.ico | Bin 0 -> 65288 bytes src/zenserver/frontend/html/favicon.ico | Bin 65288 -> 12957 bytes src/zenserver/frontend/html/pages/page.js | 24 ++++++------------------ src/zenserver/frontend/html/zen.css | 16 +++++++++++++++- 4 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 src/zenserver/frontend/html/epicgames.ico (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/epicgames.ico b/src/zenserver/frontend/html/epicgames.ico new file mode 100644 index 000000000..1cfa301a2 Binary files /dev/null and b/src/zenserver/frontend/html/epicgames.ico differ diff --git a/src/zenserver/frontend/html/favicon.ico b/src/zenserver/frontend/html/favicon.ico index 1cfa301a2..f7fb251b5 100644 Binary files a/src/zenserver/frontend/html/favicon.ico and b/src/zenserver/frontend/html/favicon.ico differ diff --git a/src/zenserver/frontend/html/pages/page.js b/src/zenserver/frontend/html/pages/page.js index 3ec0248cb..3c2d3619a 100644 --- a/src/zenserver/frontend/html/pages/page.js +++ b/src/zenserver/frontend/html/pages/page.js @@ -70,24 +70,12 @@ export class ZenPage extends PageBase { var root = parent.tag().id("branding"); - const zen_store = root.tag("pre").id("logo").text( - "_________ _______ __\n" + - "\\____ /___ ___ / ___// |__ ___ ______ ____\n" + - " / __/ __ \\ / \\ \\___ \\\\_ __// \\\\_ \\/ __ \\\n" + - " / \\ __// | \\/ \\| | ( - )| |\\/\\ __/\n" + - "/______/\\___/\\__|__/\\______/|__| \\___/ |__| \\___|" - ); - zen_store.tag().id("go_home").on_click(() => window.location.search = ""); - - root.tag("img").attr("src", "favicon.ico").id("ue_logo"); - - /* - _________ _______ __ - \____ /___ ___ / ___// |__ ___ ______ ____ - / __/ __ \ / \ \___ \\_ __// \\_ \/ __ \ - / \ __// | \/ \| | ( - )| |\/\ __/ - /______/\___/\__|__/\______/|__| \___/ |__| \___| - */ + const logo_container = root.tag("div").id("logo"); + logo_container.tag("img").attr("src", "favicon.ico").id("zen_icon"); + logo_container.tag("span").id("zen_text").text("zenserver"); + logo_container.tag().id("go_home").on_click(() => window.location.search = ""); + + root.tag("img").attr("src", "epicgames.ico").id("epic_logo"); } set_title(...args) diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css index 34c265610..702bf9aa6 100644 --- a/src/zenserver/frontend/html/zen.css +++ b/src/zenserver/frontend/html/zen.css @@ -365,6 +365,20 @@ a { margin: auto; user-select: none; position: relative; + display: flex; + align-items: center; + gap: 0.8em; + + #zen_icon { + width: 3em; + height: 3em; + } + + #zen_text { + font-size: 2em; + font-weight: bold; + letter-spacing: 0.05em; + } #go_home { width: 100%; @@ -379,7 +393,7 @@ a { filter: drop-shadow(0 0.15em 0.1em var(--theme_p2)); } - #ue_logo { + #epic_logo { position: absolute; top: 1em; right: 0; -- cgit v1.2.3 From 0763d09a81e5a1d3df11763a7ec75e7860c9510a Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Wed, 4 Mar 2026 14:13:46 +0100 Subject: compute orchestration (#763) - Added local process runners for Linux/Wine, Mac with some sandboxing support - Horde & Nomad provisioning for development and testing - Client session queues with lifecycle management (active/draining/cancelled), automatic retry with configurable limits, and manual reschedule API - Improved web UI for orchestrator, compute, and hub dashboards with WebSocket push updates - Some security hardening - Improved scalability and `zen exec` command Still experimental - compute support is disabled by default --- src/zenserver/frontend/html/404.html | 486 +++++++++ src/zenserver/frontend/html/compute.html | 991 ------------------ src/zenserver/frontend/html/compute/banner.js | 321 ++++++ src/zenserver/frontend/html/compute/compute.html | 1072 ++++++++++++++++++++ src/zenserver/frontend/html/compute/hub.html | 310 ++++++ src/zenserver/frontend/html/compute/index.html | 1 + src/zenserver/frontend/html/compute/nav.js | 79 ++ .../frontend/html/compute/orchestrator.html | 831 +++++++++++++++ src/zenserver/frontend/html/pages/page.js | 36 + src/zenserver/frontend/html/zen.css | 27 + 10 files changed, 3163 insertions(+), 991 deletions(-) create mode 100644 src/zenserver/frontend/html/404.html delete mode 100644 src/zenserver/frontend/html/compute.html create mode 100644 src/zenserver/frontend/html/compute/banner.js create mode 100644 src/zenserver/frontend/html/compute/compute.html create mode 100644 src/zenserver/frontend/html/compute/hub.html create mode 100644 src/zenserver/frontend/html/compute/index.html create mode 100644 src/zenserver/frontend/html/compute/nav.js create mode 100644 src/zenserver/frontend/html/compute/orchestrator.html (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/404.html b/src/zenserver/frontend/html/404.html new file mode 100644 index 000000000..829ef2097 --- /dev/null +++ b/src/zenserver/frontend/html/404.html @@ -0,0 +1,486 @@ + + + + + +Ooops + + + + + + +
+ +
+ +
+

404 NOT FOUND

+
+ + + + + + diff --git a/src/zenserver/frontend/html/compute.html b/src/zenserver/frontend/html/compute.html deleted file mode 100644 index 668189fe5..000000000 --- a/src/zenserver/frontend/html/compute.html +++ /dev/null @@ -1,991 +0,0 @@ - - - - - - Zen Compute Dashboard - - - - -
-
-
-

Zen Compute Dashboard

-
Last updated: Never
-
-
-
- Checking... -
-
- -
- - -
Action Queue
-
-
-
Pending Actions
-
-
-
Waiting to be scheduled
-
-
-
Running Actions
-
-
-
Currently executing
-
-
-
Completed Actions
-
-
-
Results available
-
-
- - -
-
Action Queue History
-
- -
-
- - -
Performance Metrics
-
-
Completion Rate
-
-
-
-
-
1 min rate
-
-
-
-
-
5 min rate
-
-
-
-
-
15 min rate
-
-
-
-
- Total Retired - - -
-
- Mean Rate - - -
-
-
- - -
Workers
-
-
Worker Status
-
- Registered Workers - - -
- -
- - -
Recent Actions
-
-
Action History
-
No actions recorded yet.
- -
- - -
System Resources
-
-
-
CPU Usage
-
-
-
Percent
-
-
-
-
- -
-
-
- Packages - - -
-
- Physical Cores - - -
-
- Logical Processors - - -
-
-
-
-
Memory
-
- Used - - -
-
- Total - - -
-
-
-
-
-
-
Disk
-
- Used - - -
-
- Total - - -
-
-
-
-
-
-
- - - - diff --git a/src/zenserver/frontend/html/compute/banner.js b/src/zenserver/frontend/html/compute/banner.js new file mode 100644 index 000000000..61c7ce21f --- /dev/null +++ b/src/zenserver/frontend/html/compute/banner.js @@ -0,0 +1,321 @@ +/** + * zen-banner.js — Zen Compute dashboard banner Web Component + * + * Usage: + * + * + * + * + * + * + * Attributes: + * variant "full" (default) | "compact" + * cluster-status "nominal" (default) | "degraded" | "offline" + * load 0–100 integer, shown as a percentage (default: hidden) + * tagline custom tagline text (default: "Orchestrator Overview" / "Orchestrator") + * subtitle text after "ZEN" in the wordmark (default: "COMPUTE") + */ + +class ZenBanner extends HTMLElement { + + static get observedAttributes() { + return ['variant', 'cluster-status', 'load', 'tagline', 'subtitle']; + } + + attributeChangedCallback() { + if (this.shadowRoot) this._render(); + } + + connectedCallback() { + if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); + this._render(); + } + + // ───────────────────────────────────────────── + // Derived values + // ───────────────────────────────────────────── + + get _variant() { return this.getAttribute('variant') || 'full'; } + get _status() { return (this.getAttribute('cluster-status') || 'nominal').toLowerCase(); } + get _load() { return this.getAttribute('load'); } // null → hidden + get _tagline() { return this.getAttribute('tagline'); } // null → default + get _subtitle() { return this.getAttribute('subtitle'); } // null → "COMPUTE" + + get _statusColor() { + return { nominal: '#7ecfb8', degraded: '#d4a84b', offline: '#c0504d' }[this._status] ?? '#7ecfb8'; + } + + get _statusLabel() { + return { nominal: 'NOMINAL', degraded: 'DEGRADED', offline: 'OFFLINE' }[this._status] ?? 'NOMINAL'; + } + + get _loadColor() { + const v = parseInt(this._load, 10); + if (isNaN(v)) return '#7ecfb8'; + if (v >= 85) return '#c0504d'; + if (v >= 60) return '#d4a84b'; + return '#7ecfb8'; + } + + // ───────────────────────────────────────────── + // Render + // ───────────────────────────────────────────── + + _render() { + const compact = this._variant === 'compact'; + this.shadowRoot.innerHTML = ` + + ${this._html(compact)} + `; + } + + // ───────────────────────────────────────────── + // CSS + // ───────────────────────────────────────────── + + _css(compact) { + const height = compact ? '60px' : '100px'; + const padding = compact ? '0 24px' : '0 32px'; + const gap = compact ? '16px' : '24px'; + const markSize = compact ? '34px' : '52px'; + const divH = compact ? '32px' : '48px'; + const nameSize = compact ? '15px' : '22px'; + const tagSize = compact ? '9px' : '11px'; + const sc = this._statusColor; + const lc = this._loadColor; + + return ` + @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@300;400&family=Space+Mono:wght@400;700&display=swap'); + + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + + :host { + display: block; + font-family: 'Space Mono', monospace; + } + + .banner { + width: 100%; + height: ${height}; + background: #0b0d10; + border: 1px solid #1e2330; + border-radius: 6px; + display: flex; + align-items: center; + padding: ${padding}; + gap: ${gap}; + position: relative; + overflow: hidden; + } + + /* scan-line texture */ + .banner::before { + content: ''; + position: absolute; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, transparent 3px, + rgba(255,255,255,0.012) 3px, rgba(255,255,255,0.012) 4px + ); + pointer-events: none; + } + + /* ambient glow */ + .banner::after { + content: ''; + position: absolute; + right: -60px; + top: 50%; + transform: translateY(-50%); + width: 280px; + height: 280px; + background: radial-gradient(circle, rgba(130,200,180,0.06) 0%, transparent 70%); + pointer-events: none; + } + + .logo-mark { + flex-shrink: 0; + width: ${markSize}; + height: ${markSize}; + } + + .logo-mark svg { width: 100%; height: 100%; } + + .divider { + width: 1px; + height: ${divH}; + background: linear-gradient(to bottom, transparent, #2a3040, transparent); + flex-shrink: 0; + } + + .text-block { + display: flex; + flex-direction: column; + gap: 4px; + } + + .wordmark { + font-weight: 700; + font-size: ${nameSize}; + letter-spacing: 0.12em; + color: #e8e4dc; + text-transform: uppercase; + line-height: 1; + } + + .wordmark span { color: #7ecfb8; } + + .tagline { + font-family: 'Noto Serif JP', serif; + font-weight: 300; + font-size: ${tagSize}; + letter-spacing: 0.3em; + color: #4a5a68; + text-transform: uppercase; + } + + .spacer { flex: 1; } + + /* ── right-side decorative circuit ── */ + .circuit { flex-shrink: 0; opacity: 0.22; } + + /* ── status cluster ── */ + .status-cluster { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 6px; + } + + .status-row { + display: flex; + align-items: center; + gap: 8px; + } + + .status-lbl { + font-size: 9px; + letter-spacing: 0.18em; + color: #3a4555; + text-transform: uppercase; + } + + .pill { + display: flex; + align-items: center; + gap: 5px; + border-radius: 20px; + padding: 2px 10px; + font-size: 10px; + letter-spacing: 0.1em; + } + + .pill.cluster { + color: ${sc}; + background: color-mix(in srgb, ${sc} 8%, transparent); + border: 1px solid color-mix(in srgb, ${sc} 28%, transparent); + } + + .pill.load-pill { + color: ${lc}; + background: color-mix(in srgb, ${lc} 8%, transparent); + border: 1px solid color-mix(in srgb, ${lc} 28%, transparent); + } + + .dot { + width: 5px; + height: 5px; + border-radius: 50%; + animation: pulse 2.4s ease-in-out infinite; + } + + .dot.cluster { background: ${sc}; } + .dot.load-dot { background: ${lc}; animation-delay: 0.5s; } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.25; } + } + `; + } + + // ───────────────────────────────────────────── + // HTML template + // ───────────────────────────────────────────── + + _html(compact) { + const loadAttr = this._load; + const showStatus = !compact; + + const rightSide = showStatus ? ` + + + + + + + + + +
+
+ Cluster +
+
+ ${this._statusLabel} +
+
+ ${loadAttr !== null ? ` +
+ Load +
+
+ ${parseInt(loadAttr, 10)} % +
+
` : ''} +
+ ` : ''; + + return ` + + `; + } + + // ───────────────────────────────────────────── + // SVG logo mark + // ───────────────────────────────────────────── + + _svgMark() { + return ` + + + + + + + + + + + + + + + + + + `; + } +} + +customElements.define('zen-banner', ZenBanner); diff --git a/src/zenserver/frontend/html/compute/compute.html b/src/zenserver/frontend/html/compute/compute.html new file mode 100644 index 000000000..1e101d839 --- /dev/null +++ b/src/zenserver/frontend/html/compute/compute.html @@ -0,0 +1,1072 @@ + + + + + + Zen Compute Dashboard + + + + + + +
+ + + Node + Orchestrator + +
Last updated: Never
+ +
+ + +
Action Queue
+
+
+
Pending Actions
+
-
+
Waiting to be scheduled
+
+
+
Running Actions
+
-
+
Currently executing
+
+
+
Completed Actions
+
-
+
Results available
+
+
+ + +
+
Action Queue History
+
+ +
+
+ + +
Performance Metrics
+
+
Completion Rate
+
+
+
-
+
1 min rate
+
+
+
-
+
5 min rate
+
+
+
-
+
15 min rate
+
+
+
+
+ Total Retired + - +
+
+ Mean Rate + - +
+
+
+ + +
Workers
+
+
Worker Status
+
+ Registered Workers + - +
+ +
+ + +
Queues
+
+
Queue Status
+
No queues.
+ +
+ + +
Recent Actions
+
+
Action History
+
No actions recorded yet.
+ +
+ + +
System Resources
+
+
+
CPU Usage
+
-
+
Percent
+
+
+
+
+ +
+
+
+ Packages + - +
+
+ Physical Cores + - +
+
+ Logical Processors + - +
+
+
+
+
Memory
+
+ Used + - +
+
+ Total + - +
+
+
+
+
+
+
Disk
+
+ Used + - +
+
+ Total + - +
+
+
+
+
+
+
+ + + + diff --git a/src/zenserver/frontend/html/compute/hub.html b/src/zenserver/frontend/html/compute/hub.html new file mode 100644 index 000000000..f66ba94d5 --- /dev/null +++ b/src/zenserver/frontend/html/compute/hub.html @@ -0,0 +1,310 @@ + + + + + + + + Zen Hub Dashboard + + + +
+ + + Hub + +
Last updated: Never
+ +
+ +
Capacity
+
+
+
Active Modules
+
-
+
Currently provisioned
+
+
+
Peak Modules
+
-
+
High watermark
+
+
+
Instance Limit
+
-
+
Maximum allowed
+
+
+
+
+
+ +
Modules
+
+
Storage Server Instances
+
No modules provisioned.
+ + + + + + + + + +
+
+ + + + diff --git a/src/zenserver/frontend/html/compute/index.html b/src/zenserver/frontend/html/compute/index.html new file mode 100644 index 000000000..9597fd7f3 --- /dev/null +++ b/src/zenserver/frontend/html/compute/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/zenserver/frontend/html/compute/nav.js b/src/zenserver/frontend/html/compute/nav.js new file mode 100644 index 000000000..8ec42abd0 --- /dev/null +++ b/src/zenserver/frontend/html/compute/nav.js @@ -0,0 +1,79 @@ +/** + * zen-nav.js — Zen dashboard navigation bar Web Component + * + * Usage: + * + * + * + * Node + * Orchestrator + * + * + * Each child becomes a nav link. The current page is + * highlighted automatically based on the href. + */ + +class ZenNav extends HTMLElement { + + connectedCallback() { + if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); + this._render(); + } + + _render() { + const currentPath = window.location.pathname; + const items = Array.from(this.querySelectorAll(':scope > a')); + + const links = items.map(a => { + const href = a.getAttribute('href') || ''; + const label = a.textContent.trim(); + const active = currentPath.endsWith(href); + return `${label}`; + }).join(''); + + this.shadowRoot.innerHTML = ` + + + `; + } +} + +customElements.define('zen-nav', ZenNav); diff --git a/src/zenserver/frontend/html/compute/orchestrator.html b/src/zenserver/frontend/html/compute/orchestrator.html new file mode 100644 index 000000000..2ee57b6b3 --- /dev/null +++ b/src/zenserver/frontend/html/compute/orchestrator.html @@ -0,0 +1,831 @@ + + + + + + + + Zen Orchestrator Dashboard + + + +
+ + + Node + Orchestrator + +
+
+
Last updated: Never
+
+
+ Agents: + - +
+
+ +
+ +
+
Compute Agents
+
No agents registered.
+ + + + + + + + + + + + + + + + + + +
+
+
Connected Clients
+
No clients connected.
+ + + + + + + + + + + + +
+
+
+
Event History
+
+ + +
+
+
+
No provisioning events recorded.
+ + + + + + + + + + + +
+ +
+
+ + + + diff --git a/src/zenserver/frontend/html/pages/page.js b/src/zenserver/frontend/html/pages/page.js index 3c2d3619a..592b699dc 100644 --- a/src/zenserver/frontend/html/pages/page.js +++ b/src/zenserver/frontend/html/pages/page.js @@ -3,6 +3,7 @@ "use strict"; import { WidgetHost } from "../util/widgets.js" +import { Fetcher } from "../util/fetcher.js" //////////////////////////////////////////////////////////////////////////////// export class PageBase extends WidgetHost @@ -63,6 +64,7 @@ export class ZenPage extends PageBase super(parent, ...args); super.set_title("zen"); this.add_branding(parent); + this.add_service_nav(parent); this.generate_crumbs(); } @@ -78,6 +80,40 @@ export class ZenPage extends PageBase root.tag("img").attr("src", "epicgames.ico").id("epic_logo"); } + add_service_nav(parent) + { + const nav = parent.tag().id("service_nav"); + + // Map service base URIs to dashboard links, this table is also used to detemine + // which links to show based on the services that are currently registered. + + const service_dashboards = [ + { base_uri: "/compute/", label: "Compute", href: "/dashboard/compute/compute.html" }, + { base_uri: "/orch/", label: "Orchestrator", href: "/dashboard/compute/orchestrator.html" }, + { base_uri: "/hub/", label: "Hub", href: "/dashboard/compute/hub.html" }, + ]; + + new Fetcher().resource("/api/").json().then((data) => { + const services = data.services || []; + const uris = new Set(services.map(s => s.base_uri)); + + const links = service_dashboards.filter(d => uris.has(d.base_uri)); + + if (links.length === 0) + { + nav.inner().style.display = "none"; + return; + } + + for (const link of links) + { + nav.tag("a").text(link.label).attr("href", link.href); + } + }).catch(() => { + nav.inner().style.display = "none"; + }); + } + set_title(...args) { super.set_title(...args); diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css index 702bf9aa6..a80a1a4f6 100644 --- a/src/zenserver/frontend/html/zen.css +++ b/src/zenserver/frontend/html/zen.css @@ -80,6 +80,33 @@ input { } } +/* service nav -------------------------------------------------------------- */ + +#service_nav { + display: flex; + justify-content: center; + gap: 0.3em; + margin-bottom: 1.5em; + padding: 0.3em; + background-color: var(--theme_g3); + border: 1px solid var(--theme_g2); + border-radius: 0.4em; + + a { + padding: 0.3em 0.9em; + border-radius: 0.3em; + font-size: 0.85em; + color: var(--theme_g1); + text-decoration: none; + } + + a:hover { + background-color: var(--theme_p4); + color: var(--theme_g0); + text-decoration: none; + } +} + /* links -------------------------------------------------------------------- */ a { -- cgit v1.2.3 From b37b34ea6ad906f54e8104526e77ba66aed997da Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Mon, 9 Mar 2026 17:43:08 +0100 Subject: Dashboard overhaul, compute integration (#814) - **Frontend dashboard overhaul**: Unified compute/main dashboards into a single shared UI. Added new pages for cache, projects, metrics, sessions, info (build/runtime config, system stats). Added live-update via WebSockets with pause control, sortable detail tables, themed styling. Refactored compute/hub/orchestrator pages into modular JS. - **HTTP server fixes and stats**: Fixed http.sys local-only fallback when default port is in use, implemented root endpoint redirect for http.sys, fixed Linux/Mac port reuse. Added /stats endpoint exposing HTTP server metrics (bytes transferred, request rates). Added WebSocket stats tracking. - **OTEL/diagnostics hardening**: Improved OTLP HTTP exporter with better error handling and resilience. Extended diagnostics services configuration. - **Session management**: Added new sessions service with HTTP endpoints for registering, updating, querying, and removing sessions. Includes session log file support. This is still WIP. - **CLI subcommand support**: Added support for commands with subcommands in the zen CLI tool, with improved command dispatch. - **Misc**: Exposed CPU usage/hostname to frontend, fixed JS compact binary float32/float64 decoding, limited projects displayed on front page to 25 sorted by last access, added vscode:// link support. Also contains some fixes from TSAN analysis. --- src/zenserver/frontend/html/banner.js | 338 +++++++++ src/zenserver/frontend/html/compute/banner.js | 321 -------- src/zenserver/frontend/html/compute/compute.html | 327 +++------ src/zenserver/frontend/html/compute/hub.html | 154 +--- src/zenserver/frontend/html/compute/nav.js | 79 -- .../frontend/html/compute/orchestrator.html | 205 +----- src/zenserver/frontend/html/index.html | 3 + src/zenserver/frontend/html/nav.js | 79 ++ src/zenserver/frontend/html/pages/cache.js | 690 ++++++++++++++++++ src/zenserver/frontend/html/pages/compute.js | 693 ++++++++++++++++++ src/zenserver/frontend/html/pages/entry.js | 4 +- src/zenserver/frontend/html/pages/hub.js | 122 ++++ src/zenserver/frontend/html/pages/info.js | 261 +++++++ src/zenserver/frontend/html/pages/map.js | 4 +- src/zenserver/frontend/html/pages/metrics.js | 232 ++++++ src/zenserver/frontend/html/pages/oplog.js | 2 +- src/zenserver/frontend/html/pages/orchestrator.js | 405 +++++++++++ src/zenserver/frontend/html/pages/page.js | 69 +- src/zenserver/frontend/html/pages/project.js | 2 +- src/zenserver/frontend/html/pages/projects.js | 447 ++++++++++++ src/zenserver/frontend/html/pages/sessions.js | 61 ++ src/zenserver/frontend/html/pages/start.js | 327 ++++++--- src/zenserver/frontend/html/pages/stat.js | 2 +- src/zenserver/frontend/html/pages/tree.js | 2 +- src/zenserver/frontend/html/pages/zcache.js | 8 +- src/zenserver/frontend/html/theme.js | 116 +++ src/zenserver/frontend/html/util/compactbinary.js | 4 +- src/zenserver/frontend/html/util/friendly.js | 21 + src/zenserver/frontend/html/util/widgets.js | 138 +++- src/zenserver/frontend/html/zen.css | 809 +++++++++++++++++---- 30 files changed, 4699 insertions(+), 1226 deletions(-) create mode 100644 src/zenserver/frontend/html/banner.js delete mode 100644 src/zenserver/frontend/html/compute/banner.js delete mode 100644 src/zenserver/frontend/html/compute/nav.js create mode 100644 src/zenserver/frontend/html/nav.js create mode 100644 src/zenserver/frontend/html/pages/cache.js create mode 100644 src/zenserver/frontend/html/pages/compute.js create mode 100644 src/zenserver/frontend/html/pages/hub.js create mode 100644 src/zenserver/frontend/html/pages/info.js create mode 100644 src/zenserver/frontend/html/pages/metrics.js create mode 100644 src/zenserver/frontend/html/pages/orchestrator.js create mode 100644 src/zenserver/frontend/html/pages/projects.js create mode 100644 src/zenserver/frontend/html/pages/sessions.js create mode 100644 src/zenserver/frontend/html/theme.js (limited to 'src/zenserver/frontend/html') diff --git a/src/zenserver/frontend/html/banner.js b/src/zenserver/frontend/html/banner.js new file mode 100644 index 000000000..2e878dedf --- /dev/null +++ b/src/zenserver/frontend/html/banner.js @@ -0,0 +1,338 @@ +/** + * zen-banner.js — Zen dashboard banner Web Component + * + * Usage: + * + * + * + * + * + * + * Attributes: + * variant "full" (default) | "compact" + * cluster-status "nominal" (default) | "degraded" | "offline" + * load 0–100 integer, shown as a percentage (default: hidden) + * tagline custom tagline text (default: "Orchestrator Overview" / "Orchestrator") + * subtitle text after "ZEN" in the wordmark (default: "COMPUTE") + */ + +class ZenBanner extends HTMLElement { + + static get observedAttributes() { + return ['variant', 'cluster-status', 'load', 'tagline', 'subtitle', 'logo-src']; + } + + attributeChangedCallback() { + if (this.shadowRoot) this._render(); + } + + connectedCallback() { + if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); + this._render(); + } + + // ───────────────────────────────────────────── + // Derived values + // ───────────────────────────────────────────── + + get _variant() { return this.getAttribute('variant') || 'full'; } + get _status() { return (this.getAttribute('cluster-status') || 'nominal').toLowerCase(); } + get _load() { return this.getAttribute('load'); } // null → hidden + get _tagline() { return this.getAttribute('tagline'); } // null → default + get _subtitle() { return this.getAttribute('subtitle'); } // null → "COMPUTE" + get _logoSrc() { return this.getAttribute('logo-src'); } // null → inline SVG + + get _statusColor() { + return { nominal: '#7ecfb8', degraded: '#d4a84b', offline: '#c0504d' }[this._status] ?? '#7ecfb8'; + } + + get _statusLabel() { + return { nominal: 'NOMINAL', degraded: 'DEGRADED', offline: 'OFFLINE' }[this._status] ?? 'NOMINAL'; + } + + get _loadColor() { + const v = parseInt(this._load, 10); + if (isNaN(v)) return '#7ecfb8'; + if (v >= 85) return '#c0504d'; + if (v >= 60) return '#d4a84b'; + return '#7ecfb8'; + } + + // ───────────────────────────────────────────── + // Render + // ───────────────────────────────────────────── + + _render() { + const compact = this._variant === 'compact'; + this.shadowRoot.innerHTML = ` + + ${this._html(compact)} + `; + } + + // ───────────────────────────────────────────── + // CSS + // ───────────────────────────────────────────── + + _css(compact) { + const height = compact ? '60px' : '100px'; + const padding = compact ? '0 24px' : '0 32px'; + const gap = compact ? '16px' : '24px'; + const markSize = compact ? '34px' : '52px'; + const divH = compact ? '32px' : '48px'; + const nameSize = compact ? '15px' : '22px'; + const tagSize = compact ? '9px' : '11px'; + const sc = this._statusColor; + const lc = this._loadColor; + + return ` + @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@300;400&family=Space+Mono:wght@400;700&display=swap'); + + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + + :host { + display: block; + font-family: 'Space Mono', monospace; + } + + .banner { + width: 100%; + height: ${height}; + background: var(--theme_g3, #0b0d10); + border: 1px solid var(--theme_g2, #1e2330); + border-radius: 6px; + display: flex; + align-items: center; + padding: ${padding}; + gap: ${gap}; + position: relative; + overflow: hidden; + text-decoration: none; + color: inherit; + cursor: pointer; + } + + /* scan-line texture */ + .banner::before { + content: ''; + position: absolute; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, transparent 3px, + rgba(255,255,255,0.012) 3px, rgba(255,255,255,0.012) 4px + ); + pointer-events: none; + } + + /* ambient glow */ + .banner::after { + content: ''; + position: absolute; + right: -60px; + top: 50%; + transform: translateY(-50%); + width: 280px; + height: 280px; + background: radial-gradient(circle, rgba(130,200,180,0.06) 0%, transparent 70%); + pointer-events: none; + } + + .logo-mark { + flex-shrink: 0; + width: ${markSize}; + height: ${markSize}; + } + + .logo-mark svg, .logo-mark img { width: 100%; height: 100%; object-fit: contain; } + + .divider { + width: 1px; + height: ${divH}; + background: linear-gradient(to bottom, transparent, var(--theme_g2, #2a3040), transparent); + flex-shrink: 0; + } + + .text-block { + display: flex; + flex-direction: column; + gap: 4px; + } + + .wordmark { + font-weight: 700; + font-size: ${nameSize}; + letter-spacing: 0.12em; + color: var(--theme_bright, #e8e4dc); + text-transform: uppercase; + line-height: 1; + } + + .wordmark span { color: #7ecfb8; } + + .tagline { + font-family: 'Noto Serif JP', serif; + font-weight: 300; + font-size: ${tagSize}; + letter-spacing: 0.3em; + color: var(--theme_faint, #4a5a68); + text-transform: uppercase; + } + + .spacer { flex: 1; } + + /* ── right-side decorative circuit ── */ + .circuit { flex-shrink: 0; opacity: 0.22; } + + /* ── status cluster ── */ + .status-cluster { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 6px; + } + + .status-row { + display: flex; + align-items: center; + gap: 8px; + } + + .status-lbl { + font-size: 9px; + letter-spacing: 0.18em; + color: var(--theme_faint, #3a4555); + text-transform: uppercase; + } + + .pill { + display: flex; + align-items: center; + gap: 5px; + border-radius: 20px; + padding: 2px 10px; + font-size: 10px; + letter-spacing: 0.1em; + } + + .pill.cluster { + color: ${sc}; + background: color-mix(in srgb, ${sc} 8%, transparent); + border: 1px solid color-mix(in srgb, ${sc} 28%, transparent); + } + + .pill.load-pill { + color: ${lc}; + background: color-mix(in srgb, ${lc} 8%, transparent); + border: 1px solid color-mix(in srgb, ${lc} 28%, transparent); + } + + .dot { + width: 5px; + height: 5px; + border-radius: 50%; + animation: pulse 2.4s ease-in-out infinite; + } + + .dot.cluster { background: ${sc}; } + .dot.load-dot { background: ${lc}; animation-delay: 0.5s; } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.25; } + } + `; + } + + // ───────────────────────────────────────────── + // HTML template + // ───────────────────────────────────────────── + + _html(compact) { + const loadAttr = this._load; + const hasCluster = !compact && this.hasAttribute('cluster-status'); + const hasLoad = !compact && loadAttr !== null; + const showRight = hasCluster || hasLoad; + + const circuit = showRight ? ` + + + + + + + + ` : ''; + + const clusterRow = hasCluster ? ` +
+ Cluster +
+
+ ${this._statusLabel} +
+
` : ''; + + const loadRow = hasLoad ? ` +
+ Load +
+
+ ${parseInt(loadAttr, 10)} % +
+
` : ''; + + const rightSide = showRight ? ` + ${circuit} +
+ ${clusterRow} + ${loadRow} +
+ ` : ''; + + return ` + + `; + } + + // ───────────────────────────────────────────── + // SVG logo mark + // ───────────────────────────────────────────── + + _logoMark() { + const src = this._logoSrc; + if (src) { + return `zen`; + } + return ` + + + + + + + + + + + + + + + + + + `; + } +} + +customElements.define('zen-banner', ZenBanner); diff --git a/src/zenserver/frontend/html/compute/banner.js b/src/zenserver/frontend/html/compute/banner.js deleted file mode 100644 index 61c7ce21f..000000000 --- a/src/zenserver/frontend/html/compute/banner.js +++ /dev/null @@ -1,321 +0,0 @@ -/** - * zen-banner.js — Zen Compute dashboard banner Web Component - * - * Usage: - * - * - * - * - * - * - * Attributes: - * variant "full" (default) | "compact" - * cluster-status "nominal" (default) | "degraded" | "offline" - * load 0–100 integer, shown as a percentage (default: hidden) - * tagline custom tagline text (default: "Orchestrator Overview" / "Orchestrator") - * subtitle text after "ZEN" in the wordmark (default: "COMPUTE") - */ - -class ZenBanner extends HTMLElement { - - static get observedAttributes() { - return ['variant', 'cluster-status', 'load', 'tagline', 'subtitle']; - } - - attributeChangedCallback() { - if (this.shadowRoot) this._render(); - } - - connectedCallback() { - if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); - this._render(); - } - - // ───────────────────────────────────────────── - // Derived values - // ───────────────────────────────────────────── - - get _variant() { return this.getAttribute('variant') || 'full'; } - get _status() { return (this.getAttribute('cluster-status') || 'nominal').toLowerCase(); } - get _load() { return this.getAttribute('load'); } // null → hidden - get _tagline() { return this.getAttribute('tagline'); } // null → default - get _subtitle() { return this.getAttribute('subtitle'); } // null → "COMPUTE" - - get _statusColor() { - return { nominal: '#7ecfb8', degraded: '#d4a84b', offline: '#c0504d' }[this._status] ?? '#7ecfb8'; - } - - get _statusLabel() { - return { nominal: 'NOMINAL', degraded: 'DEGRADED', offline: 'OFFLINE' }[this._status] ?? 'NOMINAL'; - } - - get _loadColor() { - const v = parseInt(this._load, 10); - if (isNaN(v)) return '#7ecfb8'; - if (v >= 85) return '#c0504d'; - if (v >= 60) return '#d4a84b'; - return '#7ecfb8'; - } - - // ───────────────────────────────────────────── - // Render - // ───────────────────────────────────────────── - - _render() { - const compact = this._variant === 'compact'; - this.shadowRoot.innerHTML = ` - - ${this._html(compact)} - `; - } - - // ───────────────────────────────────────────── - // CSS - // ───────────────────────────────────────────── - - _css(compact) { - const height = compact ? '60px' : '100px'; - const padding = compact ? '0 24px' : '0 32px'; - const gap = compact ? '16px' : '24px'; - const markSize = compact ? '34px' : '52px'; - const divH = compact ? '32px' : '48px'; - const nameSize = compact ? '15px' : '22px'; - const tagSize = compact ? '9px' : '11px'; - const sc = this._statusColor; - const lc = this._loadColor; - - return ` - @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@300;400&family=Space+Mono:wght@400;700&display=swap'); - - *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } - - :host { - display: block; - font-family: 'Space Mono', monospace; - } - - .banner { - width: 100%; - height: ${height}; - background: #0b0d10; - border: 1px solid #1e2330; - border-radius: 6px; - display: flex; - align-items: center; - padding: ${padding}; - gap: ${gap}; - position: relative; - overflow: hidden; - } - - /* scan-line texture */ - .banner::before { - content: ''; - position: absolute; - inset: 0; - background: repeating-linear-gradient( - 0deg, - transparent, transparent 3px, - rgba(255,255,255,0.012) 3px, rgba(255,255,255,0.012) 4px - ); - pointer-events: none; - } - - /* ambient glow */ - .banner::after { - content: ''; - position: absolute; - right: -60px; - top: 50%; - transform: translateY(-50%); - width: 280px; - height: 280px; - background: radial-gradient(circle, rgba(130,200,180,0.06) 0%, transparent 70%); - pointer-events: none; - } - - .logo-mark { - flex-shrink: 0; - width: ${markSize}; - height: ${markSize}; - } - - .logo-mark svg { width: 100%; height: 100%; } - - .divider { - width: 1px; - height: ${divH}; - background: linear-gradient(to bottom, transparent, #2a3040, transparent); - flex-shrink: 0; - } - - .text-block { - display: flex; - flex-direction: column; - gap: 4px; - } - - .wordmark { - font-weight: 700; - font-size: ${nameSize}; - letter-spacing: 0.12em; - color: #e8e4dc; - text-transform: uppercase; - line-height: 1; - } - - .wordmark span { color: #7ecfb8; } - - .tagline { - font-family: 'Noto Serif JP', serif; - font-weight: 300; - font-size: ${tagSize}; - letter-spacing: 0.3em; - color: #4a5a68; - text-transform: uppercase; - } - - .spacer { flex: 1; } - - /* ── right-side decorative circuit ── */ - .circuit { flex-shrink: 0; opacity: 0.22; } - - /* ── status cluster ── */ - .status-cluster { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 6px; - } - - .status-row { - display: flex; - align-items: center; - gap: 8px; - } - - .status-lbl { - font-size: 9px; - letter-spacing: 0.18em; - color: #3a4555; - text-transform: uppercase; - } - - .pill { - display: flex; - align-items: center; - gap: 5px; - border-radius: 20px; - padding: 2px 10px; - font-size: 10px; - letter-spacing: 0.1em; - } - - .pill.cluster { - color: ${sc}; - background: color-mix(in srgb, ${sc} 8%, transparent); - border: 1px solid color-mix(in srgb, ${sc} 28%, transparent); - } - - .pill.load-pill { - color: ${lc}; - background: color-mix(in srgb, ${lc} 8%, transparent); - border: 1px solid color-mix(in srgb, ${lc} 28%, transparent); - } - - .dot { - width: 5px; - height: 5px; - border-radius: 50%; - animation: pulse 2.4s ease-in-out infinite; - } - - .dot.cluster { background: ${sc}; } - .dot.load-dot { background: ${lc}; animation-delay: 0.5s; } - - @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.25; } - } - `; - } - - // ───────────────────────────────────────────── - // HTML template - // ───────────────────────────────────────────── - - _html(compact) { - const loadAttr = this._load; - const showStatus = !compact; - - const rightSide = showStatus ? ` - - - - - - - - - -
-
- Cluster -
-
- ${this._statusLabel} -
-
- ${loadAttr !== null ? ` -
- Load -
-
- ${parseInt(loadAttr, 10)} % -
-
` : ''} -
- ` : ''; - - return ` - - `; - } - - // ───────────────────────────────────────────── - // SVG logo mark - // ───────────────────────────────────────────── - - _svgMark() { - return ` - - - - - - - - - - - - - - - - - - `; - } -} - -customElements.define('zen-banner', ZenBanner); diff --git a/src/zenserver/frontend/html/compute/compute.html b/src/zenserver/frontend/html/compute/compute.html index 1e101d839..66c20175f 100644 --- a/src/zenserver/frontend/html/compute/compute.html +++ b/src/zenserver/frontend/html/compute/compute.html @@ -5,101 +5,13 @@ Zen Compute Dashboard - - + + + + -
- +
+ + Home Node Orchestrator @@ -369,15 +226,15 @@ -