aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2026-04-11 15:44:36 +0200
committerGitHub Enterprise <[email protected]>2026-04-11 15:44:36 +0200
commit3f3d716de0b9f229092119fbfdcf5532a9cc26b2 (patch)
tree7c97c5754e05845b065371d1518f10c95f90b58d /src
parentremoved s3 test program (#942) (diff)
downloadzen-3f3d716de0b9f229092119fbfdcf5532a9cc26b2.tar.xz
zen-3f3d716de0b9f229092119fbfdcf5532a9cc26b2.zip
Dashboard stats tiles no longer flicker (#943)
Diffstat (limited to 'src')
-rw-r--r--src/zenserver/frontend/html/pages/builds.js54
-rw-r--r--src/zenserver/frontend/html/pages/cache.js213
-rw-r--r--src/zenserver/frontend/html/pages/page.js35
-rw-r--r--src/zenserver/frontend/html/pages/projects.js83
-rw-r--r--src/zenserver/frontend/html/pages/start.js1
-rw-r--r--src/zenserver/frontend/html/pages/workspaces.js1
6 files changed, 197 insertions, 190 deletions
diff --git a/src/zenserver/frontend/html/pages/builds.js b/src/zenserver/frontend/html/pages/builds.js
index 6b3426378..c63d13b91 100644
--- a/src/zenserver/frontend/html/pages/builds.js
+++ b/src/zenserver/frontend/html/pages/builds.js
@@ -39,6 +39,7 @@ export class Page extends ZenPage
_render_stats(stats)
{
+ stats = this._merge_last_stats(stats);
const grid = this._stats_grid;
const safe = (obj, path) => path.split(".").reduce((a, b) => a && a[b], obj);
@@ -49,39 +50,30 @@ export class Page extends ZenPage
// Build Store tile
{
- const blobs = safe(stats, "store.blobs");
- const metadata = safe(stats, "store.metadata");
- if (blobs || metadata)
- {
- const tile = grid.tag().classify("card").classify("stats-tile");
- tile.tag().classify("card-title").text("Build Store");
- const columns = tile.tag().classify("tile-columns");
+ const blobs = safe(stats, "store.blobs") || {};
+ const metadata = safe(stats, "store.metadata") || {};
+ const tile = grid.tag().classify("card").classify("stats-tile");
+ tile.tag().classify("card-title").text("Build Store");
+ const columns = tile.tag().classify("tile-columns");
- const left = columns.tag().classify("tile-metrics");
- this._metric(left, Friendly.bytes(safe(stats, "store.size.disk") || 0), "disk", true);
- if (blobs)
- {
- this._metric(left, Friendly.sep(blobs.count || 0), "blobs");
- this._metric(left, Friendly.sep(blobs.readcount || 0), "blob reads");
- this._metric(left, Friendly.sep(blobs.writecount || 0), "blob writes");
- const blobHitRatio = (blobs.readcount || 0) > 0
- ? (((blobs.hitcount || 0) / blobs.readcount) * 100).toFixed(1) + "%"
- : "-";
- this._metric(left, blobHitRatio, "blob hit ratio");
- }
+ const left = columns.tag().classify("tile-metrics");
+ this._metric(left, Friendly.bytes(safe(stats, "store.size.disk") || 0), "disk", true);
+ this._metric(left, Friendly.sep(blobs.count || 0), "blobs");
+ this._metric(left, Friendly.sep(blobs.readcount || 0), "blob reads");
+ this._metric(left, Friendly.sep(blobs.writecount || 0), "blob writes");
+ const blobHitRatio = (blobs.readcount || 0) > 0
+ ? (((blobs.hitcount || 0) / blobs.readcount) * 100).toFixed(1) + "%"
+ : "-";
+ this._metric(left, blobHitRatio, "blob hit ratio");
- const right = columns.tag().classify("tile-metrics");
- if (metadata)
- {
- this._metric(right, Friendly.sep(metadata.count || 0), "metadata entries", true);
- this._metric(right, Friendly.sep(metadata.readcount || 0), "meta reads");
- this._metric(right, Friendly.sep(metadata.writecount || 0), "meta writes");
- const metaHitRatio = (metadata.readcount || 0) > 0
- ? (((metadata.hitcount || 0) / metadata.readcount) * 100).toFixed(1) + "%"
- : "-";
- this._metric(right, metaHitRatio, "meta hit ratio");
- }
- }
+ const right = columns.tag().classify("tile-metrics");
+ this._metric(right, Friendly.sep(metadata.count || 0), "metadata entries", true);
+ this._metric(right, Friendly.sep(metadata.readcount || 0), "meta reads");
+ this._metric(right, Friendly.sep(metadata.writecount || 0), "meta writes");
+ const metaHitRatio = (metadata.readcount || 0) > 0
+ ? (((metadata.hitcount || 0) / metadata.readcount) * 100).toFixed(1) + "%"
+ : "-";
+ this._metric(right, metaHitRatio, "meta hit ratio");
}
}
diff --git a/src/zenserver/frontend/html/pages/cache.js b/src/zenserver/frontend/html/pages/cache.js
index c6567f0be..58e2023f9 100644
--- a/src/zenserver/frontend/html/pages/cache.js
+++ b/src/zenserver/frontend/html/pages/cache.js
@@ -123,49 +123,46 @@ export class Page extends ZenPage
_render_stats(stats)
{
+ stats = this._merge_last_stats(stats);
const safe = (obj, path) => path.split(".").reduce((a, b) => a && a[b], obj);
const grid = this._stats_grid;
- this._last_stats = stats;
grid.inner().innerHTML = "";
// Store I/O tile
{
- const store = safe(stats, "cache.store");
- if (store)
- {
- const tile = grid.tag().classify("card").classify("stats-tile").classify("stats-tile-detailed");
- if (this._selected_category === "store") tile.classify("stats-tile-selected");
- tile.on_click(() => this._select_category("store"));
- tile.tag().classify("card-title").text("Store I/O");
- const columns = tile.tag().classify("tile-columns");
-
- const left = columns.tag().classify("tile-metrics");
- const storeHits = store.hits || 0;
- const storeMisses = store.misses || 0;
- const storeTotal = storeHits + storeMisses;
- const storeRatio = storeTotal > 0 ? ((storeHits / storeTotal) * 100).toFixed(1) + "%" : "-";
- this._metric(left, storeRatio, "store hit ratio", true);
- this._metric(left, Friendly.sep(storeHits), "hits");
- this._metric(left, Friendly.sep(storeMisses), "misses");
- this._metric(left, Friendly.sep(store.writes || 0), "writes");
- this._metric(left, Friendly.sep(store.rejected_reads || 0), "rejected reads");
- this._metric(left, Friendly.sep(store.rejected_writes || 0), "rejected writes");
-
- const right = columns.tag().classify("tile-metrics");
- const readRateMean = safe(store, "read.bytes.rate_mean") || 0;
- const readRate1 = safe(store, "read.bytes.rate_1") || 0;
- const readRate5 = safe(store, "read.bytes.rate_5") || 0;
- const writeRateMean = safe(store, "write.bytes.rate_mean") || 0;
- const writeRate1 = safe(store, "write.bytes.rate_1") || 0;
- const writeRate5 = safe(store, "write.bytes.rate_5") || 0;
- this._metric(right, Friendly.bytes(readRateMean) + "/s", "read rate (mean)", true);
- this._metric(right, Friendly.bytes(readRate1) + "/s", "read rate (1m)");
- this._metric(right, Friendly.bytes(readRate5) + "/s", "read rate (5m)");
- this._metric(right, Friendly.bytes(writeRateMean) + "/s", "write rate (mean)");
- this._metric(right, Friendly.bytes(writeRate1) + "/s", "write rate (1m)");
- this._metric(right, Friendly.bytes(writeRate5) + "/s", "write rate (5m)");
- }
+ const store = safe(stats, "cache.store") || {};
+ const tile = grid.tag().classify("card").classify("stats-tile").classify("stats-tile-detailed");
+ if (this._selected_category === "store") tile.classify("stats-tile-selected");
+ tile.on_click(() => this._select_category("store"));
+ tile.tag().classify("card-title").text("Store I/O");
+ const columns = tile.tag().classify("tile-columns");
+
+ const left = columns.tag().classify("tile-metrics");
+ const storeHits = store.hits || 0;
+ const storeMisses = store.misses || 0;
+ const storeTotal = storeHits + storeMisses;
+ const storeRatio = storeTotal > 0 ? ((storeHits / storeTotal) * 100).toFixed(1) + "%" : "-";
+ this._metric(left, storeRatio, "store hit ratio", true);
+ this._metric(left, Friendly.sep(storeHits), "hits");
+ this._metric(left, Friendly.sep(storeMisses), "misses");
+ this._metric(left, Friendly.sep(store.writes || 0), "writes");
+ this._metric(left, Friendly.sep(store.rejected_reads || 0), "rejected reads");
+ this._metric(left, Friendly.sep(store.rejected_writes || 0), "rejected writes");
+
+ const right = columns.tag().classify("tile-metrics");
+ const readRateMean = safe(store, "read.bytes.rate_mean") || 0;
+ const readRate1 = safe(store, "read.bytes.rate_1") || 0;
+ const readRate5 = safe(store, "read.bytes.rate_5") || 0;
+ const writeRateMean = safe(store, "write.bytes.rate_mean") || 0;
+ const writeRate1 = safe(store, "write.bytes.rate_1") || 0;
+ const writeRate5 = safe(store, "write.bytes.rate_5") || 0;
+ this._metric(right, Friendly.bytes(readRateMean) + "/s", "read rate (mean)", true);
+ this._metric(right, Friendly.bytes(readRate1) + "/s", "read rate (1m)");
+ this._metric(right, Friendly.bytes(readRate5) + "/s", "read rate (5m)");
+ this._metric(right, Friendly.bytes(writeRateMean) + "/s", "write rate (mean)");
+ this._metric(right, Friendly.bytes(writeRate1) + "/s", "write rate (1m)");
+ this._metric(right, Friendly.bytes(writeRate5) + "/s", "write rate (5m)");
}
// Hit/Miss tile
@@ -201,89 +198,83 @@ export class Page extends ZenPage
// HTTP Requests tile
{
- const req = safe(stats, "requests");
- if (req)
- {
- const tile = grid.tag().classify("card").classify("stats-tile");
- tile.tag().classify("card-title").text("HTTP Requests");
- const columns = tile.tag().classify("tile-columns");
+ const req = safe(stats, "requests") || {};
+ const tile = grid.tag().classify("card").classify("stats-tile");
+ tile.tag().classify("card-title").text("HTTP Requests");
+ const columns = tile.tag().classify("tile-columns");
- const left = columns.tag().classify("tile-metrics");
- const reqData = req.requests || req;
- this._metric(left, Friendly.sep(reqData.count || 0), "total requests", true);
- if (reqData.rate_mean > 0)
- {
- this._metric(left, Friendly.sep(reqData.rate_mean, 1) + "/s", "req/sec (mean)");
- }
- if (reqData.rate_1 > 0)
- {
- this._metric(left, Friendly.sep(reqData.rate_1, 1) + "/s", "req/sec (1m)");
- }
- if (reqData.rate_5 > 0)
- {
- this._metric(left, Friendly.sep(reqData.rate_5, 1) + "/s", "req/sec (5m)");
- }
- if (reqData.rate_15 > 0)
- {
- this._metric(left, Friendly.sep(reqData.rate_15, 1) + "/s", "req/sec (15m)");
- }
- const badRequests = safe(stats, "cache.badrequestcount") || 0;
- this._metric(left, Friendly.sep(badRequests), "bad requests");
+ const left = columns.tag().classify("tile-metrics");
+ const reqData = req.requests || req;
+ this._metric(left, Friendly.sep(reqData.count || 0), "total requests", true);
+ if (reqData.rate_mean > 0)
+ {
+ this._metric(left, Friendly.sep(reqData.rate_mean, 1) + "/s", "req/sec (mean)");
+ }
+ if (reqData.rate_1 > 0)
+ {
+ this._metric(left, Friendly.sep(reqData.rate_1, 1) + "/s", "req/sec (1m)");
+ }
+ if (reqData.rate_5 > 0)
+ {
+ this._metric(left, Friendly.sep(reqData.rate_5, 1) + "/s", "req/sec (5m)");
+ }
+ if (reqData.rate_15 > 0)
+ {
+ this._metric(left, Friendly.sep(reqData.rate_15, 1) + "/s", "req/sec (15m)");
+ }
+ const badRequests = safe(stats, "cache.badrequestcount") || 0;
+ this._metric(left, Friendly.sep(badRequests), "bad requests");
- const right = columns.tag().classify("tile-metrics");
- this._metric(right, Friendly.duration(reqData.t_avg || 0), "avg latency", true);
- if (reqData.t_p75)
- {
- this._metric(right, Friendly.duration(reqData.t_p75), "p75");
- }
- if (reqData.t_p95)
- {
- this._metric(right, Friendly.duration(reqData.t_p95), "p95");
- }
- if (reqData.t_p99)
- {
- this._metric(right, Friendly.duration(reqData.t_p99), "p99");
- }
- if (reqData.t_p999)
- {
- this._metric(right, Friendly.duration(reqData.t_p999), "p999");
- }
- if (reqData.t_max)
- {
- this._metric(right, Friendly.duration(reqData.t_max), "max");
- }
+ const right = columns.tag().classify("tile-metrics");
+ this._metric(right, Friendly.duration(reqData.t_avg || 0), "avg latency", true);
+ if (reqData.t_p75)
+ {
+ this._metric(right, Friendly.duration(reqData.t_p75), "p75");
+ }
+ if (reqData.t_p95)
+ {
+ this._metric(right, Friendly.duration(reqData.t_p95), "p95");
+ }
+ if (reqData.t_p99)
+ {
+ this._metric(right, Friendly.duration(reqData.t_p99), "p99");
+ }
+ if (reqData.t_p999)
+ {
+ this._metric(right, Friendly.duration(reqData.t_p999), "p999");
+ }
+ if (reqData.t_max)
+ {
+ this._metric(right, Friendly.duration(reqData.t_max), "max");
}
}
// RPC tile
{
- const rpc = safe(stats, "cache.rpc");
- if (rpc)
- {
- const tile = grid.tag().classify("card").classify("stats-tile");
- tile.tag().classify("card-title").text("RPC");
- const columns = tile.tag().classify("tile-columns");
+ const rpc = safe(stats, "cache.rpc") || {};
+ const tile = grid.tag().classify("card").classify("stats-tile");
+ tile.tag().classify("card-title").text("RPC");
+ const columns = tile.tag().classify("tile-columns");
- const left = columns.tag().classify("tile-metrics");
- this._metric(left, Friendly.sep(rpc.count || 0), "rpc calls", true);
- this._metric(left, Friendly.sep(rpc.ops || 0), "batch ops");
+ const left = columns.tag().classify("tile-metrics");
+ this._metric(left, Friendly.sep(rpc.count || 0), "rpc calls", true);
+ this._metric(left, Friendly.sep(rpc.ops || 0), "batch ops");
- const right = columns.tag().classify("tile-metrics");
- if (rpc.records)
- {
- this._metric(right, Friendly.sep(rpc.records.count || 0), "record calls");
- this._metric(right, Friendly.sep(rpc.records.ops || 0), "record ops");
- }
- if (rpc.values)
- {
- this._metric(right, Friendly.sep(rpc.values.count || 0), "value calls");
- this._metric(right, Friendly.sep(rpc.values.ops || 0), "value ops");
- }
- if (rpc.chunks)
- {
- this._metric(right, Friendly.sep(rpc.chunks.count || 0), "chunk calls");
- this._metric(right, Friendly.sep(rpc.chunks.ops || 0), "chunk ops");
- }
+ const right = columns.tag().classify("tile-metrics");
+ if (rpc.records)
+ {
+ this._metric(right, Friendly.sep(rpc.records.count || 0), "record calls");
+ this._metric(right, Friendly.sep(rpc.records.ops || 0), "record ops");
+ }
+ if (rpc.values)
+ {
+ this._metric(right, Friendly.sep(rpc.values.count || 0), "value calls");
+ this._metric(right, Friendly.sep(rpc.values.ops || 0), "value ops");
+ }
+ if (rpc.chunks)
+ {
+ this._metric(right, Friendly.sep(rpc.chunks.count || 0), "chunk calls");
+ this._metric(right, Friendly.sep(rpc.chunks.ops || 0), "chunk ops");
}
}
@@ -306,7 +297,7 @@ export class Page extends ZenPage
this._metric(right, safe(stats, "cid.size.large") != null ? Friendly.bytes(safe(stats, "cid.size.large")) : "-", "cid large");
}
- // Upstream tile (only if upstream is active)
+ // Upstream tile (only shown when upstream is active)
{
const upstream = safe(stats, "upstream");
if (upstream)
diff --git a/src/zenserver/frontend/html/pages/page.js b/src/zenserver/frontend/html/pages/page.js
index ff530ff8e..3653abb0e 100644
--- a/src/zenserver/frontend/html/pages/page.js
+++ b/src/zenserver/frontend/html/pages/page.js
@@ -6,6 +6,26 @@ import { WidgetHost } from "../util/widgets.js"
import { Fetcher } from "../util/fetcher.js"
import { Friendly } from "../util/friendly.js"
+function _deep_merge_stats(base, update)
+{
+ const result = Object.assign({}, base);
+ for (const key of Object.keys(update))
+ {
+ const bv = result[key];
+ const uv = update[key];
+ if (uv && typeof uv === "object" && !Array.isArray(uv)
+ && bv && typeof bv === "object" && !Array.isArray(bv))
+ {
+ result[key] = _deep_merge_stats(bv, uv);
+ }
+ else
+ {
+ result[key] = uv;
+ }
+ }
+ return result;
+}
+
////////////////////////////////////////////////////////////////////////////////
export class PageBase extends WidgetHost
{
@@ -282,10 +302,7 @@ export class ZenPage extends PageBase
_render_http_requests_tile(grid, req, bad_requests = undefined)
{
- if (!req)
- {
- return;
- }
+ req = req || {};
const tile = grid.tag().classify("card").classify("stats-tile");
tile.tag().classify("card-title").text("HTTP Requests");
const columns = tile.tag().classify("tile-columns");
@@ -338,6 +355,16 @@ export class ZenPage extends PageBase
}
}
+ _merge_last_stats(stats)
+ {
+ if (this._last_stats)
+ {
+ stats = _deep_merge_stats(this._last_stats, stats);
+ }
+ this._last_stats = stats;
+ return stats;
+ }
+
_collapsible_section(name)
{
const section = this.add_section(name);
diff --git a/src/zenserver/frontend/html/pages/projects.js b/src/zenserver/frontend/html/pages/projects.js
index e613086a9..af7c5396a 100644
--- a/src/zenserver/frontend/html/pages/projects.js
+++ b/src/zenserver/frontend/html/pages/projects.js
@@ -88,6 +88,7 @@ export class Page extends ZenPage
_render_stats(stats)
{
+ stats = this._merge_last_stats(stats);
const safe = (obj, path) => path.split(".").reduce((a, b) => a && a[b], obj);
const grid = this._stats_grid;
@@ -98,54 +99,48 @@ export class Page extends ZenPage
// Store Operations tile
{
- const store = safe(stats, "store");
- if (store)
- {
- const tile = grid.tag().classify("card").classify("stats-tile");
- tile.tag().classify("card-title").text("Store Operations");
- const columns = tile.tag().classify("tile-columns");
-
- const left = columns.tag().classify("tile-metrics");
- const proj = store.project || {};
- this._metric(left, Friendly.sep(proj.readcount || 0), "project reads", true);
- this._metric(left, Friendly.sep(proj.writecount || 0), "project writes");
- this._metric(left, Friendly.sep(proj.deletecount || 0), "project deletes");
-
- const right = columns.tag().classify("tile-metrics");
- const oplog = store.oplog || {};
- this._metric(right, Friendly.sep(oplog.readcount || 0), "oplog reads", true);
- this._metric(right, Friendly.sep(oplog.writecount || 0), "oplog writes");
- this._metric(right, Friendly.sep(oplog.deletecount || 0), "oplog deletes");
- }
+ const store = safe(stats, "store") || {};
+ const tile = grid.tag().classify("card").classify("stats-tile");
+ tile.tag().classify("card-title").text("Store Operations");
+ const columns = tile.tag().classify("tile-columns");
+
+ const left = columns.tag().classify("tile-metrics");
+ const proj = store.project || {};
+ this._metric(left, Friendly.sep(proj.readcount || 0), "project reads", true);
+ this._metric(left, Friendly.sep(proj.writecount || 0), "project writes");
+ this._metric(left, Friendly.sep(proj.deletecount || 0), "project deletes");
+
+ const right = columns.tag().classify("tile-metrics");
+ const oplog = store.oplog || {};
+ this._metric(right, Friendly.sep(oplog.readcount || 0), "oplog reads", true);
+ this._metric(right, Friendly.sep(oplog.writecount || 0), "oplog writes");
+ this._metric(right, Friendly.sep(oplog.deletecount || 0), "oplog deletes");
}
// Op & Chunk tile
{
- const store = safe(stats, "store");
- if (store)
- {
- const tile = grid.tag().classify("card").classify("stats-tile");
- tile.tag().classify("card-title").text("Ops & Chunks");
- const columns = tile.tag().classify("tile-columns");
-
- const left = columns.tag().classify("tile-metrics");
- const op = store.op || {};
- const opTotal = (op.hitcount || 0) + (op.misscount || 0);
- const opRatio = opTotal > 0 ? (((op.hitcount || 0) / opTotal) * 100).toFixed(1) + "%" : "-";
- this._metric(left, opRatio, "op hit ratio", true);
- this._metric(left, Friendly.sep(op.hitcount || 0), "op hits");
- this._metric(left, Friendly.sep(op.misscount || 0), "op misses");
- this._metric(left, Friendly.sep(op.writecount || 0), "op writes");
-
- const right = columns.tag().classify("tile-metrics");
- const chunk = store.chunk || {};
- const chunkTotal = (chunk.hitcount || 0) + (chunk.misscount || 0);
- const chunkRatio = chunkTotal > 0 ? (((chunk.hitcount || 0) / chunkTotal) * 100).toFixed(1) + "%" : "-";
- this._metric(right, chunkRatio, "chunk hit ratio", true);
- this._metric(right, Friendly.sep(chunk.hitcount || 0), "chunk hits");
- this._metric(right, Friendly.sep(chunk.misscount || 0), "chunk misses");
- this._metric(right, Friendly.sep(chunk.writecount || 0), "chunk writes");
- }
+ const store = safe(stats, "store") || {};
+ const tile = grid.tag().classify("card").classify("stats-tile");
+ tile.tag().classify("card-title").text("Ops & Chunks");
+ const columns = tile.tag().classify("tile-columns");
+
+ const left = columns.tag().classify("tile-metrics");
+ const op = store.op || {};
+ const opTotal = (op.hitcount || 0) + (op.misscount || 0);
+ const opRatio = opTotal > 0 ? (((op.hitcount || 0) / opTotal) * 100).toFixed(1) + "%" : "-";
+ this._metric(left, opRatio, "op hit ratio", true);
+ this._metric(left, Friendly.sep(op.hitcount || 0), "op hits");
+ this._metric(left, Friendly.sep(op.misscount || 0), "op misses");
+ this._metric(left, Friendly.sep(op.writecount || 0), "op writes");
+
+ const right = columns.tag().classify("tile-metrics");
+ const chunk = store.chunk || {};
+ const chunkTotal = (chunk.hitcount || 0) + (chunk.misscount || 0);
+ const chunkRatio = chunkTotal > 0 ? (((chunk.hitcount || 0) / chunkTotal) * 100).toFixed(1) + "%" : "-";
+ this._metric(right, chunkRatio, "chunk hit ratio", true);
+ this._metric(right, Friendly.sep(chunk.hitcount || 0), "chunk hits");
+ this._metric(right, Friendly.sep(chunk.misscount || 0), "chunk misses");
+ this._metric(right, Friendly.sep(chunk.writecount || 0), "chunk writes");
}
// Storage tile
diff --git a/src/zenserver/frontend/html/pages/start.js b/src/zenserver/frontend/html/pages/start.js
index 9a3eb6de3..d06040b2f 100644
--- a/src/zenserver/frontend/html/pages/start.js
+++ b/src/zenserver/frontend/html/pages/start.js
@@ -131,6 +131,7 @@ export class Page extends ZenPage
_render_stats(all_stats)
{
+ all_stats = this._merge_last_stats(all_stats);
const grid = this._stats_grid;
const safe_lookup = this._safe_lookup;
diff --git a/src/zenserver/frontend/html/pages/workspaces.js b/src/zenserver/frontend/html/pages/workspaces.js
index 2442fb35b..1668e096f 100644
--- a/src/zenserver/frontend/html/pages/workspaces.js
+++ b/src/zenserver/frontend/html/pages/workspaces.js
@@ -200,6 +200,7 @@ export class Page extends ZenPage
_render_stats(stats)
{
+ stats = this._merge_last_stats(stats);
const grid = this._stats_grid;
grid.inner().innerHTML = "";