aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/frontend/html/pages/start.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenserver/frontend/html/pages/start.js')
-rw-r--r--src/zenserver/frontend/html/pages/start.js327
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)