diff options
Diffstat (limited to 'src/zenserver/frontend/html/pages/start.js')
| -rw-r--r-- | src/zenserver/frontend/html/pages/start.js | 327 |
1 files changed, 244 insertions, 83 deletions
diff --git a/src/zenserver/frontend/html/pages/start.js b/src/zenserver/frontend/html/pages/start.js index 4c8789431..3a68a725d 100644 --- a/src/zenserver/frontend/html/pages/start.js +++ b/src/zenserver/frontend/html/pages/start.js @@ -13,109 +13,117 @@ export class Page extends ZenPage { async main() { + // Discover which services are available + const api_data = await new Fetcher().resource("/api/").json(); + const available = new Set((api_data.services || []).map(s => s.base_uri)); + // project list - var section = this.add_section("projects"); + var project_table = null; + if (available.has("/prj/")) + { + var section = this.add_section("Cooked Projects"); - section.tag().classify("dropall").text("drop-all").on_click(() => this.drop_all("projects")); + section.tag().classify("dropall").text("drop-all").on_click(() => this.drop_all("projects")); - var columns = [ - "name", - "project_dir", - "engine_dir", - "actions", - ]; - var project_table = section.add_widget(Table, columns); + var columns = [ + "name", + "project_dir", + "engine_dir", + "actions", + ]; + project_table = section.add_widget(Table, columns); - for (const project of await new Fetcher().resource("/prj/list").json()) - { - var row = project_table.add_row( - "", - project.ProjectRootDir, - project.EngineRootDir, - ); + var projects = await new Fetcher().resource("/prj/list").json(); + projects.sort((a, b) => (b.LastAccessTime || 0) - (a.LastAccessTime || 0)); + projects = projects.slice(0, 25); + projects.sort((a, b) => a.Id.localeCompare(b.Id)); - var cell = row.get_cell(0); - cell.tag().text(project.Id).on_click((x) => this.view_project(x), project.Id); + for (const project of projects) + { + var row = project_table.add_row( + "", + project.ProjectRootDir, + project.EngineRootDir, + ); + + var cell = row.get_cell(0); + cell.tag().text(project.Id).on_click((x) => this.view_project(x), project.Id); - var cell = row.get_cell(-1); - var action_tb = new Toolbar(cell, true); - action_tb.left().add("view").on_click((x) => this.view_project(x), project.Id); - action_tb.left().add("drop").on_click((x) => this.drop_project(x), project.Id); + var cell = row.get_cell(-1); + var action_tb = new Toolbar(cell, true); + action_tb.left().add("view").on_click((x) => this.view_project(x), project.Id); + action_tb.left().add("drop").on_click((x) => this.drop_project(x), project.Id); - row.attr("zs_name", project.Id); + row.attr("zs_name", project.Id); + } } // cache - var section = this.add_section("z$"); - - section.tag().classify("dropall").text("drop-all").on_click(() => this.drop_all("z$")); - - columns = [ - "namespace", - "dir", - "buckets", - "entries", - "size disk", - "size mem", - "actions", - ] - var zcache_info = new Fetcher().resource("/z$/").json(); - const cache_table = section.add_widget(Table, columns, Table.Flag_FitLeft|Table.Flag_PackRight); - for (const namespace of (await zcache_info)["Namespaces"]) + var cache_table = null; + if (available.has("/z$/")) { - new Fetcher().resource(`/z$/${namespace}/`).json().then((data) => { - const row = cache_table.add_row( - "", - data["Configuration"]["RootDir"], - data["Buckets"].length, - data["EntryCount"], - Friendly.kib(data["StorageSize"].DiskSize), - Friendly.kib(data["StorageSize"].MemorySize) - ); - var cell = row.get_cell(0); - cell.tag().text(namespace).on_click(() => this.view_zcache(namespace)); - row.get_cell(1).tag().text(namespace); + var section = this.add_section("Cache"); - cell = row.get_cell(-1); - const action_tb = new Toolbar(cell, true); - action_tb.left().add("view").on_click(() => this.view_zcache(namespace)); - action_tb.left().add("drop").on_click(() => this.drop_zcache(namespace)); + section.tag().classify("dropall").text("drop-all").on_click(() => this.drop_all("z$")); - row.attr("zs_name", namespace); - }); + var columns = [ + "namespace", + "dir", + "buckets", + "entries", + "size disk", + "size mem", + "actions", + ]; + var zcache_info = await new Fetcher().resource("/z$/").json(); + cache_table = section.add_widget(Table, columns, Table.Flag_FitLeft|Table.Flag_PackRight); + for (const namespace of zcache_info["Namespaces"] || []) + { + new Fetcher().resource(`/z$/${namespace}/`).json().then((data) => { + const row = cache_table.add_row( + "", + data["Configuration"]["RootDir"], + data["Buckets"].length, + data["EntryCount"], + Friendly.bytes(data["StorageSize"].DiskSize), + Friendly.bytes(data["StorageSize"].MemorySize) + ); + var cell = row.get_cell(0); + cell.tag().text(namespace).on_click(() => this.view_zcache(namespace)); + row.get_cell(1).tag().text(namespace); + + cell = row.get_cell(-1); + const action_tb = new Toolbar(cell, true); + action_tb.left().add("view").on_click(() => this.view_zcache(namespace)); + action_tb.left().add("drop").on_click(() => this.drop_zcache(namespace)); + + row.attr("zs_name", namespace); + }); + } } - // stats + // stats tiles const safe_lookup = (obj, path, pretty=undefined) => { const ret = path.split(".").reduce((a,b) => a && a[b], obj); - if (ret === undefined) return "-"; + if (ret === undefined) return undefined; return pretty ? pretty(ret) : ret; }; - section = this.add_section("stats"); - columns = [ - "name", - "req count", - "size disk", - "size mem", - "cid total", - ]; - const stats_table = section.add_widget(Table, columns, Table.Flag_PackRight); - var providers = new Fetcher().resource("stats").json(); - for (var provider of (await providers)["providers"]) - { - var stats = await new Fetcher().resource("stats", provider).json(); - var size_stat = (stats.store || stats.cache); - var values = [ - "", - safe_lookup(stats, "requests.count"), - safe_lookup(size_stat, "size.disk", Friendly.kib), - safe_lookup(size_stat, "size.memory", Friendly.kib), - safe_lookup(stats, "cid.size.total"), - ]; - row = stats_table.add_row(...values); - row.get_cell(0).tag().text(provider).on_click((x) => this.view_stat(x), provider); - } + var section = this.add_section("Stats"); + section.tag().classify("dropall").text("metrics dashboard →").on_click(() => { + window.location = "?page=metrics"; + }); + + var providers_data = await new Fetcher().resource("stats").json(); + var provider_list = providers_data["providers"] || []; + var all_stats = {}; + await Promise.all(provider_list.map(async (provider) => { + all_stats[provider] = await new Fetcher().resource("stats", provider).json(); + })); + + this._stats_grid = section.tag().classify("grid").classify("stats-tiles"); + this._safe_lookup = safe_lookup; + this._render_stats(all_stats); // version var ver_tag = this.tag().id("version"); @@ -125,6 +133,159 @@ export class Page extends ZenPage this._project_table = project_table; this._cache_table = cache_table; + + // WebSocket for live stats updates + this._connect_stats_ws(); + } + + _connect_stats_ws() + { + try + { + const proto = location.protocol === "https:" ? "wss:" : "ws:"; + const ws = new WebSocket(`${proto}//${location.host}/stats`); + + try { this._ws_paused = localStorage.getItem("zen-ws-paused") === "true"; } catch (e) { this._ws_paused = false; } + document.addEventListener("zen-ws-toggle", (e) => { + this._ws_paused = e.detail.paused; + }); + + ws.onmessage = (ev) => { + if (this._ws_paused) + { + return; + } + try + { + const all_stats = JSON.parse(ev.data); + this._render_stats(all_stats); + } + catch (e) { /* ignore parse errors */ } + }; + + ws.onclose = () => { this._stats_ws = null; }; + ws.onerror = () => { ws.close(); }; + + this._stats_ws = ws; + } + catch (e) { /* WebSocket not available */ } + } + + _render_stats(all_stats) + { + const grid = this._stats_grid; + const safe_lookup = this._safe_lookup; + + // Clear existing tiles + grid.inner().innerHTML = ""; + + // HTTP tile — aggregate request stats across all providers + { + const tile = grid.tag().classify("card").classify("stats-tile"); + tile.tag().classify("card-title").text("HTTP"); + const columns = tile.tag().classify("tile-columns"); + + // Left column: request stats + const left = columns.tag().classify("tile-metrics"); + + let total_requests = 0; + let total_rate = 0; + for (const p in all_stats) + { + total_requests += (safe_lookup(all_stats[p], "requests.count") || 0); + total_rate += (safe_lookup(all_stats[p], "requests.rate_1") || 0); + } + + this._add_tile_metric(left, Friendly.sep(total_requests), "total requests", true); + if (total_rate > 0) + this._add_tile_metric(left, Friendly.sep(total_rate, 1) + "/s", "req/sec (1m)"); + + // Right column: websocket stats + const ws = all_stats["http"] ? (all_stats["http"]["websockets"] || {}) : {}; + const right = columns.tag().classify("tile-metrics"); + + this._add_tile_metric(right, Friendly.sep(ws.active_connections || 0), "ws connections", true); + const ws_frames = (ws.frames_received || 0) + (ws.frames_sent || 0); + if (ws_frames > 0) + this._add_tile_metric(right, Friendly.sep(ws_frames), "ws frames"); + const ws_bytes = (ws.bytes_received || 0) + (ws.bytes_sent || 0); + if (ws_bytes > 0) + this._add_tile_metric(right, Friendly.bytes(ws_bytes), "ws traffic"); + + tile.on_click(() => { window.location = "?page=metrics"; }); + } + + // Cache tile (z$) + if (all_stats["z$"]) + { + const s = all_stats["z$"]; + const tile = grid.tag().classify("card").classify("stats-tile"); + tile.tag().classify("card-title").text("Cache"); + const body = tile.tag().classify("tile-metrics"); + + const hits = safe_lookup(s, "cache.hits") || 0; + const misses = safe_lookup(s, "cache.misses") || 0; + const ratio = (hits + misses) > 0 ? ((hits / (hits + misses)) * 100).toFixed(1) + "%" : "-"; + + this._add_tile_metric(body, ratio, "hit ratio", true); + this._add_tile_metric(body, safe_lookup(s, "cache.size.disk", Friendly.bytes) || "-", "disk"); + this._add_tile_metric(body, safe_lookup(s, "cache.size.memory", Friendly.bytes) || "-", "memory"); + + tile.on_click(() => { window.location = "?page=stat&provider=z$"; }); + } + + // Project Store tile (prj) + if (all_stats["prj"]) + { + const s = all_stats["prj"]; + const tile = grid.tag().classify("card").classify("stats-tile"); + tile.tag().classify("card-title").text("Project Store"); + const body = tile.tag().classify("tile-metrics"); + + this._add_tile_metric(body, safe_lookup(s, "requests.count", Friendly.sep) || "-", "requests", true); + this._add_tile_metric(body, safe_lookup(s, "store.size.disk", Friendly.bytes) || "-", "disk"); + + tile.on_click(() => { window.location = "?page=stat&provider=prj"; }); + } + + // Build Store tile (builds) + if (all_stats["builds"]) + { + const s = all_stats["builds"]; + const tile = grid.tag().classify("card").classify("stats-tile"); + tile.tag().classify("card-title").text("Build Store"); + const body = tile.tag().classify("tile-metrics"); + + this._add_tile_metric(body, safe_lookup(s, "requests.count", Friendly.sep) || "-", "requests", true); + this._add_tile_metric(body, safe_lookup(s, "store.size.disk", Friendly.bytes) || "-", "disk"); + + tile.on_click(() => { window.location = "?page=stat&provider=builds"; }); + } + + // Workspace tile (ws) + if (all_stats["ws"]) + { + const s = all_stats["ws"]; + const tile = grid.tag().classify("card").classify("stats-tile"); + tile.tag().classify("card-title").text("Workspace"); + const body = tile.tag().classify("tile-metrics"); + + this._add_tile_metric(body, safe_lookup(s, "requests.count", Friendly.sep) || "-", "requests", true); + this._add_tile_metric(body, safe_lookup(s, "workspaces.filescount", Friendly.sep) || "-", "files"); + + tile.on_click(() => { window.location = "?page=stat&provider=ws"; }); + } + } + + _add_tile_metric(parent, value, label, hero=false) + { + const m = parent.tag().classify("tile-metric"); + if (hero) + { + m.classify("tile-metric-hero"); + } + m.tag().classify("metric-value").text(value); + m.tag().classify("metric-label").text(label); } view_stat(provider) |