aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Ridgers <[email protected]>2024-10-03 13:54:59 +0200
committerGitHub Enterprise <[email protected]>2024-10-03 13:54:59 +0200
commit61ec0fbdf3f87a41486e5d5dfde5d23e91941b42 (patch)
tree66775461556506cd39c84256efe3f119c29fecfc
parentcache get command (#183) (diff)
downloadzen-61ec0fbdf3f87a41486e5d5dfde5d23e91941b42.tar.xz
zen-61ec0fbdf3f87a41486e5d5dfde5d23e91941b42.zip
- Improvement: Self-hosted dashboard (#181)
Self-hosted dashboard gets oplog entry view and a stats browser
-rw-r--r--CHANGELOG.md3
-rw-r--r--src/zenserver/frontend/html.zipbin87642 -> 116085 bytes
-rw-r--r--src/zenserver/frontend/html/compactbinary.js440
-rw-r--r--src/zenserver/frontend/html/index.html1
-rw-r--r--src/zenserver/frontend/html/zen.css41
-rw-r--r--src/zenserver/frontend/html/zen.js840
6 files changed, 1164 insertions, 161 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48be794cd..db4577499 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
##
- Feature: Added command `zen cache-get` to fetch a cache value/record or an attachment from a cache record
+- Improvement: Self-hosted dashboard
+ - Oplog entry view is more complete
+ - Separate page for inspecting server stats
## 5.5.8
- Feature: Added option `gc-attachment-passes` to zenserver
diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip
index 11e039bc8..9080591e5 100644
--- a/src/zenserver/frontend/html.zip
+++ b/src/zenserver/frontend/html.zip
Binary files differ
diff --git a/src/zenserver/frontend/html/compactbinary.js b/src/zenserver/frontend/html/compactbinary.js
new file mode 100644
index 000000000..c648e6052
--- /dev/null
+++ b/src/zenserver/frontend/html/compactbinary.js
@@ -0,0 +1,440 @@
+"use strict";
+
+////////////////////////////////////////////////////////////////////////////////
+class VarInt
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+VarInt.measure = function(data_view)
+{
+ var value = data_view[0];
+ var ret = 1;
+ for (; value & 0x80; value <<= 1, ++ret);
+ return ret;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+VarInt.read_uint = function(data_view, return_type=Number)
+{
+ const length = VarInt.measure(data_view);
+ var value = return_type(data_view[0] & (0xff >> length));
+ for (var i = 1; i < length; ++i)
+ {
+ value <<= return_type(8);
+ value |= return_type(data_view[i]);
+ }
+ return [value, length];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+VarInt.read_int = function(data_view, return_type=Number)
+{
+ var [value, length] = VarInt.read_uint(data_view, return_type);
+ value = -(value & return_type(1)) ^ (value >> return_type(1));
+ return [value, length];
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+function cb_assert(expr_result)
+{
+ if (Boolean(expr_result) == false)
+ throw Error("compactbinary error");
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+const CbFieldType = {
+ None : 0x00,
+ Null : 0x01,
+ Object : 0x02,
+ UniformObject : 0x03,
+ Array : 0x04,
+ UniformArray : 0x05,
+ Binary : 0x06,
+ String : 0x07,
+ IntegerPositive : 0x08,
+ IntegerNegative : 0x09,
+ Float32 : 0x0a,
+ Float64 : 0x0b,
+ BoolFalse : 0x0c,
+ BoolTrue : 0x0d,
+ ObjectAttachment : 0x0e,
+ BinaryAttachment : 0x0f,
+ Hash : 0x10,
+ Uuid : 0x11,
+ DateTime : 0x12,
+ TimeSpan : 0x13,
+ ObjectId : 0x14,
+ CustomById : 0x1e,
+ CustomByName : 0x1f,
+ Reserved : 0x20,
+ HasFieldType : 0x40,
+ HasFieldName : 0x80,
+}
+
+////////////////////////////////////////////////////////////////////////////////
+class CbFieldTypeOps
+{
+ static SerializedTypeMask = 0b10111111;
+ static TypeMask = 0b00111111;
+ static ObjectMask = 0b00111110;
+ static ObjectBase = 0b00000010;
+ static ArrayMask = 0b00111110;
+ static ArrayBase = 0b00000100;
+ static IntegerMask = 0b00111110;
+ static IntegerBase = 0b00001000;
+ static FloatMask = 0b00111100;
+ static FloatBase = 0b00001000;
+ static BoolMask = 0b00111110;
+ static BoolBase = 0b00001100;
+ static AttachmentMask = 0b00111110;
+ static AttachmentBase = 0b00001110;
+
+ static get_type(type) { return type & CbFieldTypeOps.TypeMask; }
+ static get_serialized_type(type) { return type & CbFieldTypeOps.SerializedTypeMask; }
+ static has_field_type(type) { return (type & CbFieldType.HasFieldType) != 0; }
+ static has_field_name(type) { return (type & CbFieldType.HasFieldName) != 0; }
+ static is_none(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.None; }
+ static is_null(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.Null; }
+ static is_object(type) { return (type & CbFieldTypeOps.ObjectMask) == CbFieldTypeOps.ObjectBase; }
+ static is_array(type) { return (type & CbFieldTypeOps.ArrayMask) == CbFieldTypeOps.ArrayBase; }
+ static is_binary(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.Binary; }
+ static is_string(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.String; }
+ static is_integer(type) { return (type & CbFieldTypeOps.IntegerMask) == CbFieldTypeOps.IntegerBase; }
+ static is_float(type) { return (type & CbFieldTypeOps.FloatMask) == CbFieldTypeOps.FloatBase; }
+ static is_bool(type) { return (type & CbFieldTypeOps.BoolMask) == CbFieldTypeOps.BoolBase; }
+ static is_object_attachment(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.ObjectAttachment; }
+ static is_binary_attachment(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.BinaryAttachment; }
+ static is_attachment(type) { return (type & CbFieldTypeOps.AttachmentMask) == CbFieldTypeOps.AttachmentBase; }
+ static is_uuid(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.Uuid; }
+ static is_object_id(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.ObjectId; }
+ static is_custom_by_id(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.CustomById; }
+ static is_custom_by_name(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.CustomByName; }
+ static is_date_time(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.DateTime; }
+ static is_time_span(type) { return CbFieldTypeOps.get_type(type) == CbFieldType.TimeSpan; }
+ static is_hash(type) { var t = CbFieldTypeOps.get_type(type); return t >= CbFieldType.ObjectAttachment && t <= CbFieldType.Hash; }
+ static may_contain_attachments(type){ var t = CbFieldTypeOps.get_type(type); return is_object(t) || is_array(t) || is_attachement(t); }
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+class CbFieldView
+{
+ constructor()
+ {
+ this._type = CbFieldType.None;
+ this._name = "";
+ this._data_view = undefined;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype._from_field = function(field)
+{
+ this._type = field._type;
+ this._name = field._name;
+ this._data_view = field._data_view;
+ return this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype._from_data = function(data_view, type=CbFieldType.HasFieldType)
+{
+ if (CbFieldTypeOps.has_field_type(type))
+ {
+ type = data_view[0] | CbFieldType.HasFieldType;
+ data_view = data_view.subarray(1);
+ }
+
+ if (CbFieldTypeOps.has_field_name(type))
+ {
+ const [n, varint_len] = VarInt.read_uint(data_view);
+ this._name = new TextDecoder().decode(data_view.subarray(varint_len, n + varint_len));
+ data_view = data_view.subarray(n + varint_len);
+ }
+
+ this._type = type;
+ this._data_view = data_view;
+ return this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView._iterate = function*(data_view, uniform_type)
+{
+ while (data_view.length > 0)
+ {
+ const field = new CbFieldView()._from_data(data_view, uniform_type);
+ yield field;
+
+ const field_size = field.get_payload_size();
+ cb_assert(field_size <= data_view.length);
+ data_view = field.get_payload().subarray(field_size);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype.get_type = function()
+{
+ return this._type;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype.get_name = function()
+{
+ return this._name;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype.get_payload = function()
+{
+ return this._data_view;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype.get_payload_size = function()
+{
+ switch (CbFieldTypeOps.get_type(this.get_type()))
+ {
+ case CbFieldType.None:
+ case CbFieldType.Null:
+ return 0;
+ case CbFieldType.Object:
+ case CbFieldType.UniformObject:
+ case CbFieldType.Array:
+ case CbFieldType.UniformArray:
+ case CbFieldType.Binary:
+ case CbFieldType.String:
+ const [value, varint_len] = VarInt.read_uint(this._data_view);
+ return value + varint_len;
+ case CbFieldType.IntegerPositive:
+ case CbFieldType.IntegerNegative:
+ return VarInt.measure(this._data_view);
+ case CbFieldType.Float32:
+ return 4;
+ case CbFieldType.Float64:
+ return 8;
+ case CbFieldType.BoolFalse:
+ case CbFieldType.BoolTrue:
+ return 0;
+ case CbFieldType.ObjectAttachment:
+ case CbFieldType.BinaryAttachment:
+ case CbFieldType.Hash:
+ return 20;
+ case CbFieldType.Uuid:
+ return 16;
+ case CbFieldType.ObjectId:
+ return 12;
+ case CbFieldType.DateTime:
+ case CbFieldType.TimeSpan:
+ return 8;
+ }
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype._is = function(func) { return func(this.get_type()); }
+CbFieldView.prototype.is_null = function() { return this._is(CbFieldTypeOps.is_null); }
+CbFieldView.prototype.is_object = function() { return this._is(CbFieldTypeOps.is_object); }
+CbFieldView.prototype.is_array = function() { return this._is(CbFieldTypeOps.is_array); }
+CbFieldView.prototype.is_binary = function() { return this._is(CbFieldTypeOps.is_binary); }
+CbFieldView.prototype.is_string = function() { return this._is(CbFieldTypeOps.is_string); }
+CbFieldView.prototype.is_integer = function() { return this._is(CbFieldTypeOps.is_integer); }
+CbFieldView.prototype.is_float = function() { return this._is(CbFieldTypeOps.is_float); }
+CbFieldView.prototype.is_bool = function() { return this._is(CbFieldTypeOps.is_bool); }
+CbFieldView.prototype.is_object_attachment = function() { return this._is(CbFieldTypeOps.is_object_attachment); }
+CbFieldView.prototype.is_binary_attachment = function() { return this._is(CbFieldTypeOps.is_binary_attachment); }
+CbFieldView.prototype.is_attachment = function() { return this._is(CbFieldTypeOps.is_attachment); }
+CbFieldView.prototype.is_hash = function() { return this._is(CbFieldTypeOps.is_hash); }
+CbFieldView.prototype.is_uuid = function() { return this._is(CbFieldTypeOps.is_uuid); }
+CbFieldView.prototype.is_object_id = function() { return this._is(CbFieldTypeOps.is_object_id); }
+CbFieldView.prototype.is_date_time = function() { return this._is(CbFieldTypeOps.is_date_time); }
+CbFieldView.prototype.is_time_span = function() { return this._is(CbFieldTypeOps.is_time_span); }
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype.as_object = function()
+{
+ cb_assert(CbFieldTypeOps.is_object(this.get_type()));
+ return new CbObjectView()._from_field(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype.as_array = function()
+{
+ cb_assert(CbFieldTypeOps.is_array(this.get_type()));
+ return new CbArrayView()._from_field(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype.as_value = function(int_type=Number)
+{
+ switch (CbFieldTypeOps.get_type(this.get_type()))
+ {
+ case CbFieldType.None: return undefined;
+ case CbFieldType.Null: return null;
+
+ case CbFieldType.Object:
+ case CbFieldType.UniformObject: return this.as_object();
+
+ case CbFieldType.Array:
+ case CbFieldType.UniformArray: return this.as_array();
+
+ case CbFieldType.Binary: {
+ const [n, vn] = VarInt.read_uint(this._data_view);
+ return this._data_view.subarray(vn, n + vn);
+ }
+
+ case CbFieldType.String: {
+ const [n, vn] = VarInt.read_uint(this._data_view);
+ return new TextDecoder().decode(this._data_view.subarray(vn, n + vn));
+ }
+
+ case CbFieldType.IntegerPositive: return VarInt.read_uint(this._data_view, int_type)[0];
+ case CbFieldType.IntegerNegative: return VarInt.read_int(this._data_view, int_type)[0];
+
+ case CbFieldType.Float32: return new DataView(this._data_view.subarray(0, 4)).getFloat32(0, false);
+ case CbFieldType.Float64: return new DataView(this._data_view.subarray(0, 8)).getFloat64(0, false);
+ case CbFieldType.BoolFalse: return false;
+ case CbFieldType.BoolTrue: return true;
+
+ case CbFieldType.ObjectAttachment:
+ case CbFieldType.BinaryAttachment:
+ case CbFieldType.Hash: return this._data_view.subarray(0, 20);
+
+ case CbFieldType.Uuid: return this._data_view.subarray(0, 16);
+ case CbFieldType.ObjectId: return this._data_view.subarray(0, 12);
+
+ case CbFieldType.DateTime:
+ case CbFieldType.TimeSpan: return this._data_view.subarray(0, 8);
+ }
+
+ cb_assert(false);
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+class CbObjectView extends CbFieldView
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbObjectView.prototype[Symbol.iterator] = function()
+{
+ var data_view = this.get_payload();
+
+ const [payload_size, varint_len] = VarInt.read_uint(data_view);
+ if (payload_size == 0)
+ return {};
+ data_view = data_view.subarray(varint_len, payload_size + varint_len);
+
+ var uniform_type = CbFieldType.HasFieldType;
+ if (CbFieldTypeOps.get_type(this.get_type()) == CbFieldType.UniformObject)
+ {
+ uniform_type = data_view[0];
+ data_view = data_view.subarray(1);
+ }
+
+ return CbFieldView._iterate(data_view, uniform_type);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbObjectView.prototype.to_js_object = function()
+{
+ const impl = function(node)
+ {
+ if (node.is_object())
+ {
+ const ret = {};
+ for (var item of node.as_object())
+ ret[item.get_name()] = impl(item);
+ return ret;
+ }
+
+ if (node.is_array())
+ {
+ const ret = [];
+ for (var item of node.as_array())
+ ret.push(impl(item));
+ return ret;
+ }
+
+ if (node.is_string()) return node.as_value();
+ if (node.is_float()) return node.as_value();
+ if (node.is_integer()) return node.as_value();
+
+ var ret = node.as_value();
+ if (ret instanceof Uint8Array)
+ {
+ ret = "";
+ for (var x of node.as_value())
+ ret += x.toString(16).padStart(2, "0");
+ }
+ return ret;
+ };
+
+ return impl(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbObjectView.prototype.find = function(name)
+{
+ for (const field of this)
+ if (field.get_name() == name)
+ return field;
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+class CbArrayView extends CbFieldView
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbArrayView.prototype[Symbol.iterator] = function()
+{
+ var data_view = this.get_payload();
+
+ const [payload_size, varint_len] = VarInt.read_uint(data_view);
+ data_view = data_view.subarray(varint_len, payload_size + varint_len);
+
+ const item_count_bytes = VarInt.measure(data_view);
+ if (item_count_bytes >= payload_size)
+ return {};
+ data_view = data_view.subarray(item_count_bytes);
+
+ var uniform_type = CbFieldType.HasFieldType;
+ if (CbFieldTypeOps.get_type(this.get_type()) == CbFieldType.UniformArray)
+ {
+ uniform_type = data_view[0];
+ data_view = data_view.subarray(1);
+ }
+
+ return CbFieldView._iterate(data_view, uniform_type);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbArrayView.prototype.num = function()
+{
+ var data_view = this._data_view;
+ const [n, n_len] = VarInt.read_uint(data_view);
+ data_view = data_view.subarray(n_len);
+ return VarInt.read_uint(data_view)[0];
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+class CbObject extends CbFieldView
+{
+ constructor(uint8_array)
+ {
+ super();
+ this._from_data(uint8_array);
+ }
+}
diff --git a/src/zenserver/frontend/html/index.html b/src/zenserver/frontend/html/index.html
index 8bc690c22..300b8c62e 100644
--- a/src/zenserver/frontend/html/index.html
+++ b/src/zenserver/frontend/html/index.html
@@ -4,6 +4,7 @@
<head>
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" type="text/css" href="zen.css" />
+ <script src="compactbinary.js"></script>
<script src="zen.js"></script>
</head>
<body onload="main()">
diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css
index 9e6eb8f78..d9b983ff6 100644
--- a/src/zenserver/frontend/html/zen.css
+++ b/src/zenserver/frontend/html/zen.css
@@ -27,6 +27,10 @@ body {
background-color: var(--theme_g4);
}
+pre {
+ margin: 0;
+}
+
* {
box-sizing: border-box;
}
@@ -93,25 +97,33 @@ h3 {
margin-bottom: 1.2em;
}
-.zen_table > .zen_row {
+.zen_table > div {
display: contents;
}
-.zen_table > .zen_row:nth-child(odd) > .zen_cell {
+.zen_table > div:nth-of-type(odd) {
background-color: var(--theme_g3);
}
-.zen_table > .zen_row:first-child > .zen_cell {
+.zen_table > div:first-of-type {
font-weight: bold;
background-color: var(--theme_p3);
}
-.zen_table > .zen_row > .zen_cell {
+.zen_table > hidden {
+ visibility: hidden;
+ display: none;
+}
+
+.zen_table > div > div {
padding: 0.3em;
padding-left: 0.75em;
padding-right: 0.75em;
align-content: center;
border-left: 1px solid var(--theme_g2);
+ overflow: auto;
+ overflow-wrap: break-word;
+ background-color: inherit;
}
/* toolbar ------------------------------------------------------------------ */
@@ -159,6 +171,7 @@ h3 {
display: flex;
justify-content: center;
align-items: center;
+ backdrop-filter: blur(5px);
}
.zen_modal .zen_modal_bg {
@@ -169,7 +182,7 @@ h3 {
width: 100%;
height: 100%;
background: var(--theme_g0);
- opacity: 0.5;
+ opacity: 0.4;
}
.zen_modal > div {
@@ -217,6 +230,24 @@ h3 {
align-content: center;
}
+/* crumbs ------------------------------------------------------------------- */
+
+#crumbs {
+ display: flex;
+ position: relative;
+ top: -1em;
+}
+
+#crumbs > div {
+ padding-right: 0.5em;
+}
+
+#crumbs > div:nth-child(odd)::after {
+ content: ":";
+ font-weight: bolder;
+ color: var(--theme_p2);
+}
+
/* branding ----------------------------------------------------------------- */
#branding {
diff --git a/src/zenserver/frontend/html/zen.js b/src/zenserver/frontend/html/zen.js
index 6bd74de0c..3fac5f312 100644
--- a/src/zenserver/frontend/html/zen.js
+++ b/src/zenserver/frontend/html/zen.js
@@ -1,10 +1,35 @@
+"use strict";
+
////////////////////////////////////////////////////////////////////////////////
-class Component
+class Friendly
+{
+ static sep(value, prec=0)
+ {
+ return value.toLocaleString("en", {
+ style: "decimal",
+ minimumFractionDigits : prec,
+ maximumFractionDigits : prec,
+ });
+ }
+
+ static k(x) { return Friendly.sep((x + 999) / Math.pow(10, 3), 0) + "K"; }
+ static m(x) { return Friendly.sep( x / Math.pow(10, 6), 1) + "M"; }
+ static g(x) { return Friendly.sep( x / Math.pow(10, 6), 2) + "G"; }
+ static kib(x) { return Friendly.sep((x + 1023) / (1 << 10), 0) + " KiB"; }
+ static mib(x) { return Friendly.sep( x / (1 << 20), 1) + " MiB"; }
+ static gib(x) { return Friendly.sep( x / (1 << 30), 2) + " GiB"; }
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+class ComponentBase
{
constructor(element)
{
- if (element instanceof Component)
+ if (element instanceof ComponentBase)
element = element._element;
+
this._element = element;
}
@@ -17,17 +42,33 @@ class Component
{
this._element.parentNode.removeChild(this._element);
}
+}
+////////////////////////////////////////////////////////////////////////////////
+class ComponentDom extends ComponentBase
+{
tag(tag="div")
{
var element = document.createElement(tag);
this._element.appendChild(element);
- return new Component(element);
+ return this.new_component(element);
+ }
+
+ retag(new_tag)
+ {
+ if (this._element.tagName == new_tag.toUpperCase())
+ return this;
+
+ var element = document.createElement(new_tag);
+ element.innerHTML = this._element.innerHTML;
+ this._element.parentNode.replaceChild(element, this._element);
+ this._element = element;
+ return this;
}
text(value)
{
- value = String(value);
+ value = value.toString();
this._element.innerHTML = (value != "") ? value : "";
return this;
}
@@ -40,10 +81,7 @@ class Component
classify(value)
{
- var cur = this._element.className;
- cur += cur ? " " : "";
- cur += value;
- this._element.className = cur;
+ this._element.classList.add(value);
return this;
}
@@ -58,7 +96,11 @@ class Component
this._element.setAttribute(key, value);
return this;
}
+}
+////////////////////////////////////////////////////////////////////////////////
+class ComponentInteract extends ComponentDom
+{
link(resource=undefined, query_params={})
{
if (resource != undefined)
@@ -80,31 +122,71 @@ class Component
return this;
}
- on(what, func)
+ on(what, func, ...args)
{
const thunk = (src) => {
if (src.target != this._element)
return;
- func(src.target);
+ func(...args);
src.stopPropagation();
};
+
this._element.addEventListener(what, thunk);
return this;
}
- on_click(func)
+ on_click(func, ...args)
{
this.classify("zen_action");
- return this.on("click", func);
+ return this.on("click", func, ...args);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+class Component extends ComponentInteract
+{
+ new_component(...args)
+ {
+ return new Component(...args);
}
}
////////////////////////////////////////////////////////////////////////////////
-class Cell extends Component
+class TableCell extends Component
{
+ constructor(element, row)
+ {
+ super(element);
+ this._row = row
+ }
+
+ get_table() { return this.get_row().get_table(); }
+ get_row() { return this._row; }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+class TableRow extends Component
+{
+ constructor(element, table, index, cells)
+ {
+ super(element);
+ this._table = table;
+ this._index = index;
+ this._cells = cells;
+ }
+
+ *[Symbol.iterator]()
+ {
+ for (var cell of this._cells)
+ yield cell;
+ }
+
+ get_table() { return this._table; }
+ get_index() { return this._index; }
+ get_cell(index) { return this._cells.at(index); }
}
////////////////////////////////////////////////////////////////////////////////
@@ -123,35 +205,63 @@ class Table extends Component
root.css_var("zen_columns", this._num_columns);
this._num_columns = column_names.length;
- this.add_row(column_names, false);
+ this._add_row(column_names, false);
+
+ this._rows = [];
+ }
+
+ *[Symbol.iterator]()
+ {
+ for (var row of this._rows)
+ yield row;
}
- add_row(cells, indexed=true)
+ get_row(index)
{
+ return this._rows.at(index);
+ }
+
+ _add_row(cells, indexed=true)
+ {
+ var index = -1;
if (indexed && this._index >= 0)
- cells = [this._index++, ...cells];
+ {
+ index = this._index++;
+ cells = [index, ...cells];
+ }
cells = cells.slice(0, this._num_columns);
while (cells.length < this._num_columns)
cells.push("");
var ret = [];
- var row = this.tag().classify("zen_row");
+ var row = this.tag()
+ row = new TableRow(row, this, index, ret);
for (const cell of cells)
{
- var leaf = row.tag().classify("zen_cell").text(cell);
- ret.push(new Cell(leaf));
+ var leaf = row.tag().text(cell);
+ ret.push(new TableCell(leaf, row));
}
- var bias = (this._index >= 0) ? 1 : 0;
- return ret.slice(bias);
+ if (this._index >= 0)
+ ret.shift();
+
+ return row
+ }
+
+ add_row(...args)
+ {
+ var row = this._add_row(...args);
+ this._rows.push(row);
+ return row;
}
clear(index=0)
{
const elem = this._element;
elem.replaceChildren(elem.firstElementChild);
- this._index = index;
+ this._index = (this._index >= 0) ? index : -1;
+ this._rows = [];
}
}
@@ -163,14 +273,62 @@ class PropTable extends Table
constructor(parent)
{
super(parent, ["prop", "value"], -1);
+ this.classify("zen_proptable");
}
add_property(key, value)
{
- var ret = this.add_row([key, value]);
- ret[0].classify("zen_prop_key");
- ret[1].classify("zen_prop_value");
- return ret;
+ return this.add_row([key, value]);
+ }
+
+ add_object(object, friendly=false, prec=2)
+ {
+ const impl = (node, prefix="") => {
+ for (const key in node)
+ {
+ var value = node[key];
+ if (value instanceof Object &&
+ (value.constructor.name == "Object" ||
+ value.constructor.name == "Array"))
+ {
+ impl(value, prefix + key + ".");
+ continue;
+ }
+
+ if (friendly && typeof value == "number")
+ {
+ if (key.indexOf("memory") >= 0) value = Friendly.kib(value);
+ else if (key.indexOf("disk") >= 0) value = Friendly.kib(value);
+ else if (value > 100000) value = Friendly.k(value);
+ else if (value % 1) value = Friendly.sep(value, 3);
+ else value = Friendly.sep(value, 0);
+ }
+
+ this.add_property(prefix + key, value);
+ }
+ };
+
+ return impl(object);
+ }
+
+ filter(...needles)
+ {
+ for (var row of this)
+ row.retag("div")
+
+ if (needles.length == 0)
+ return;
+
+ for (var row of this)
+ {
+ var hide = false;
+ var cell = row.get_cell(0);
+ for (var needle of needles)
+ hide = hide || (cell.inner().innerHTML.indexOf(needle) < 0);
+
+ if (hide)
+ row.retag("hidden");
+ }
}
}
@@ -205,11 +363,11 @@ class Modal
return this;
}
- option(name, func)
+ option(name, func, ...args)
{
- const thunk = (src) => {
+ const thunk = () => {
this._root.destroy();
- func(src);
+ func(...args);
};
this._buttons.tag().text(name).on("click", thunk);
return this;
@@ -268,35 +426,74 @@ class Sectormatron extends Component
////////////////////////////////////////////////////////////////////////////////
-async function zen_fetch(resource, method="GET")
+class Fetcher
{
- var response = await fetch(resource, {
- "method" : method,
- "headers" : { "Accept": "application/json" },
- });
+ constructor()
+ {
+ this._resource = "";
+ this._query = {};
+ }
- return await response.json();
-}
+ resource(...parts)
+ {
+ var value = parts.join("/");
+ if (!value.startsWith("/"))
+ value= "/" + value;
+ this._resource = value;
+ return this;
+ }
-////////////////////////////////////////////////////////////////////////////////
-function zen_flatten(object, ret=null, prefix="")
-{
- if (ret == null)
- ret = new Object();
+ param(name, value)
+ {
+ this._query[name] = value;
+ return this;
+ }
+
+ async json()
+ {
+ const response = await this._get("application/json");
+ return response ? (await response.json()) : {};
+ }
- for (const key in object)
+ async cbo()
{
- const value = object[key];
- if (value instanceof Object)
+ const response = await this._get("application/x-ue-cb");
+ if (!response)
+ return null;
+
+ const buffer = await response.arrayBuffer();
+ const data = new Uint8Array(buffer);
+ return new CbObject(data);
+ }
+
+ async delete()
+ {
+ const resource = this._build_uri();
+ const response = await fetch(resource, { "method" : "DELETE" });
+ }
+
+ _build_uri()
+ {
+ var suffix = "";
+ for (var key in this._query)
{
- zen_flatten(value, ret, key + ".");
- continue;
+ suffix += suffix ? "&" : "?";
+ suffix += key + "=" + this._query[key];
}
-
- ret[prefix + key] = value;
+ return this._resource + suffix;
}
- return ret;
+ async _get(accept="*")
+ {
+ const resource = this._build_uri();
+ const response = await fetch(resource, {
+ "method" : "GET",
+ "headers" : { "Accept": accept },
+ });
+
+ if (response.status >= 200 || response.status <= 299)
+ return response;
+ }
}
@@ -313,25 +510,102 @@ class Page
set_title(name)
{
- document.title = "zen - " + name;
+ var value = document.title;
+ if (name.length && value.length)
+ name = value + " - " + name;
+ document.title = name;
}
get_param(name, fallback=undefined)
{
var ret = this._params.get(name);
- return (ret != undefined) ? ret : fallback;
+ if (ret != undefined)
+ return ret;
+
+ if (fallback != undefined)
+ this.set_param(name, fallback);
+
+ return fallback;
+ }
+
+ set_param(name, value, update=true)
+ {
+ this._params.set(name, value);
+ if (!update)
+ return value;
+
+ const url = new URL(window.location);
+ for (var [key, xfer] of this._params)
+ url.searchParams.set(key, xfer)
+ history.replaceState(null, "", url);
+
+ return value;
}
add_section(name)
{
return this._sectormatron.add_section(name);
}
+
+ reload()
+ {
+ window.location.reload();
+ }
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+class ZenPage extends Page
+{
+ constructor(...args)
+ {
+ super(...args);
+ super.set_title("zen");
+ this.generate_crumbs();
+ }
+
+ set_title(...args)
+ {
+ super.set_title(...args);
+ }
+
+ generate_crumbs()
+ {
+ const auto_name = this.constructor.name;
+ if (auto_name == "Start")
+ return;
+
+ const crumbs = this._parent.tag().id("crumbs");
+ const new_crumb = function(name, search=undefined) {
+ crumbs.tag();
+ var crumb = crumbs.tag().text(name);
+ if (search != undefined)
+ crumb.on_click((x) => window.location.search = x, search);
+ };
+
+ new_crumb("home", "");
+
+ var project = this.get_param("project");
+ if (project != undefined)
+ {
+ 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}`);
+ }
+ }
+
+ new_crumb(auto_name.toLowerCase());
+ }
}
////////////////////////////////////////////////////////////////////////////////
-class Entry extends Page
+class Entry extends ZenPage
{
async main()
{
@@ -339,36 +613,102 @@ class Entry extends Page
const project = this.get_param("project");
const oplog = this.get_param("oplog");
- const key = this.get_param("key");
- const uri = `/prj/${project}/oplog/${oplog}/entries?opkey=${key}`;
+ const opkey = this.get_param("opkey");
+
+ var entry = new Fetcher()
+ .resource("prj", project, "oplog", oplog, "entries")
+ .param("opkey", opkey)
+ .cbo()
+ entry = await entry;
+ entry = entry.as_object().find("entry").as_object();
+
+ const name = entry.find("key").as_value();
+ var section = this.add_section(name);
- var entry = await zen_fetch(uri);
- var text = JSON.stringify(entry, null, 2);
- this._parent.tag("pre").text(text);
+ // tree
+ {
+ var tree = entry.find("$tree");
+ if (tree == undefined)
+ tree = this._convert_legacy_to_tree(entry);
+
+ delete tree["$id"];
+
+ const sub_section = section.add_section("deps");
+ for (const dep_name in tree)
+ {
+ const dep_section = sub_section.add_section(dep_name);
+ const table = new Table(dep_section, ["id", "name"]);
+ for (const dep_id of tree[dep_name])
+ {
+ const cell_values = [dep_id.toString(16)];
+ const row = table.add_row(cell_values);
+ row.get_cell(0).on_click(() => void(0));
+ }
+ }
+ }
+
+ // props
+ {
+ const object = entry.to_js_object();
+ var sub_section = section.add_section("props");
+ new PropTable(sub_section).add_object(object);
+ }
+ }
+
+ _convert_legacy_to_tree(entry)
+ {
+ const tree = {};
+
+ var id = 0n;
+ for (var item of entry.find("packagedata").as_array())
+ {
+ var pkg_id = item.as_object().find("id");
+ if (pkg_id == undefined)
+ continue;
+
+ for (var x of pkg_id.as_value().subarray(0, 8))
+ {
+ id <<= 8n;
+ id |= BigInt(x);
+ }
+ break;
+ }
+ tree["$id"] = id;
+
+ const pkgst_entry = entry.find("packagestoreentry").as_object();
+ const imported = pkgst_entry.find("importedpackageids");
+ if (imported == undefined)
+ return tree;
+
+ var out = tree["imported"] = [];
+ for (var item of imported.as_array())
+ out.push(item.as_value(BigInt));
+
+ return tree;
}
}
////////////////////////////////////////////////////////////////////////////////
-class Oplog extends Page
+class Oplog extends ZenPage
{
constructor(...args)
{
super(...args);
- this._index_start = this.get_param("start", 0);
- this._index_count = this.get_param("start", 50);
+ this._index_start = Number(this.get_param("start", 0));
+ this._index_count = Number(this.get_param("count", 50));
this._entry_table = undefined;
}
async main()
{
- this.set_title("oplog");
-
const project = this.get_param("project");
const oplog = this.get_param("oplog");
+ this.set_title("oplog - " + oplog);
+
var section = this.add_section(project + " - " + oplog);
this._build_nav(section)
@@ -380,17 +720,19 @@ class Oplog extends Page
_build_nav(section)
{
var nav = new Toolbar(section);
- nav.left().add("prev").on_click(() => this._on_next_prev(-1));
- nav.left().add("next").on_click(() => this._on_next_prev( 1));
+ nav.left().add("&lt;&lt;").on_click(() => this._on_next_prev(-10));
+ nav.left().add("prev") .on_click(() => this._on_next_prev( -1));
+ nav.left().add("next") .on_click(() => this._on_next_prev( 1));
+ nav.left().add("&gt;&gt;").on_click(() => this._on_next_prev( 10));
nav.left().sep();
for (var count of [10, 25, 50, 100])
{
- var handler = (e) => this._on_change_count(e.innerHTML);
- nav.left().add(count).on_click(handler);
+ var handler = (n) => this._on_change_count(n);
+ nav.left().add(count).on_click(handler, count);
}
- nav.right().add("search:", "span");
+ nav.right().add("search:", "label");
nav.right().add("", "input").attr("disabled", "");
}
@@ -399,25 +741,29 @@ class Oplog extends Page
const project = this.get_param("project");
const oplog = this.get_param("oplog");
- var uri = `/prj/${project}/oplog/${oplog}/entries`;
- uri += "?start=" + this._index_start;
- uri += "&count=" + this._index_count;
-
- var entries = zen_fetch(uri);
+ var entries = new Fetcher()
+ .resource("prj", project, "oplog", oplog, "entries")
+ .param("start", this.set_param("start", this._index_start))
+ .param("count", this.set_param("count", this._index_count))
+ .json();
this._entry_table.clear(this._index_start);
- for (const entry of (await entries)["entries"])
+ entries = (await entries)["entries"];
+ if (entries == undefined)
+ return
+
+ for (const entry of entries)
{
- var cells = this._entry_table.add_row([
+ var row = this._entry_table.add_row([
entry["key"]
]);
- cells[0].link("", {
+ row.get_cell(0).link("", {
"page" : "entry",
"project" : project,
"oplog" : oplog,
- "key" : entry["key"],
+ "opkey" : entry["key"],
});
}
}
@@ -439,19 +785,18 @@ class Oplog extends Page
////////////////////////////////////////////////////////////////////////////////
-class Project extends Page
+class Project extends ZenPage
{
async main()
{
- this.set_title("project");
-
// info
var section = this.add_section("info");
const project = this.get_param("project");
- const prefix = "/prj/" + project;
- var info = await zen_fetch(prefix);
+ this.set_title("project - " + project);
+
+ var info = await new Fetcher().resource("prj", project).json();
var prop_table = new PropTable(section);
for (const key in info)
{
@@ -471,73 +816,44 @@ class Project extends Page
{
const name = oplog["id"];
- var cells = oplog_table.add_row([
+ var row = oplog_table.add_row([
name,
]);
- var cell = cells[0];
+ var cell = row.get_cell(0);
cell.link("", {
"page" : "oplog",
"project" : project,
"oplog" : name,
});
- var action_tb = new Toolbar(cells.at(-1), true);
- action_tb.left().add("drop").attr("zen_param", name).on_click((e) => this._on_drop(e));
- }
-
- // files
- /*
- section = this.add_section("files");
- for (const oplog of info["oplogs"])
- {
- const name = oplog["id"];
-
- var files = await zen_fetch(prefix + "/oplog/" + name + "/files");
- if (files["files"].length == 0)
- continue;
-
- var sub_section = section.add_section(name);
- var table = new Table(sub_section, [
- "id",
- "clientpath",
- "serverpath",
- ]);
- var count = 0;
- for (const file of files["files"])
- {
- table.add_row([
- file["id"],
- file["clientpath"],
- file["serverpath"],
- ]);
-
- if (++count > 10)
- break;
- }
+ cell = row.get_cell(-1);
+ var action_tb = new Toolbar(cell, true);
+ action_tb.left().add("drop").on_click((x) => this.drop_oplog(x), name);
}
- */
}
- drop()
+ drop_oplog(oplog_id)
{
- alert("\\o/");
- }
+ const drop = async () => {
+ await new Fetcher()
+ .resource("prj", this.get_param("project"), "oplog", oplog_id)
+ .delete();
+ this.reload();
+ };
- _on_drop(e)
- {
new Modal()
.title("Confirmation")
- .message(`Drop oplog '${e.getAttribute("zen_param")}'?`)
- .option("Yes", () => this.drop())
- .option("No", () => void(0))
+ .message(`Drop oplog '${oplog_id}'?`)
+ .option("Yes", () => drop())
+ .option("No")
}
}
////////////////////////////////////////////////////////////////////////////////
-class Test extends Page
+class Test extends ZenPage
{
main()
{
@@ -613,27 +929,57 @@ class Test extends Page
side.add("tb_item2");
}
- var cell = prop_table.add_property("toolbar", "");
- toolbar = new Toolbar(cell[1], true);
- toolbar.left().add("tbitem0");
- toolbar.left().add("tbitem1");
- toolbar.right().add("tbitem2");
- toolbar.right().add("tbitem3");
+ var tb_item_clicked = function(arg0, arg1) {
+ alert(arg0 + " != " + arg1);
+ };
+ var row = prop_table.add_property("toolbar", "");
+ toolbar = new Toolbar(row.get_cell(-1), true);
+ toolbar.left() .add("tbitem0").on_click(tb_item_clicked, 11, -22);
+ toolbar.left() .add("tbitem1").on_click(tb_item_clicked, 22, -33);
+ toolbar.right().add("tbitem2").on_click(tb_item_clicked, 33, -55);
+ toolbar.right().add("tbitem3").on_click(tb_item_clicked, 44, -88);
+
+ this._cbo_test();
// error
throw Error("deliberate error");
}
+
+ async _cbo_test()
+ {
+ var data = new Uint8Array(await (await fetch("/prj/list")).arrayBuffer());
+ for (var item of new CbObject(data).as_array())
+ {
+ for (var subitem of item.as_object())
+ {
+ console.log(subitem.get_name(), subitem.as_value());
+ }
+ }
+
+ data = new Uint8Array(await (await fetch("/stats")).arrayBuffer());
+ {
+ var item = new CbObject(data).as_object().find("providers").as_array();
+ console.log(item.num());
+ for (var subitem of item)
+ {
+ console.log(subitem.as_value());
+ data = new Uint8Array(await (await fetch("/stats/" + subitem.as_value())).arrayBuffer());
+ for (var ssitem of new CbObject(data).as_object())
+ {
+ console.log(ssitem.get_name(), ssitem.as_value());
+ }
+ }
+ }
+ }
}
////////////////////////////////////////////////////////////////////////////////
-class Start extends Page
+class Start extends ZenPage
{
async main()
{
- this.set_title("main");
-
var section = this.add_section("projects");
// project list
@@ -645,43 +991,224 @@ class Start extends Page
];
var table = new Table(section, columns);
- for (const project of await zen_fetch("/prj/list"))
+ for (const project of await new Fetcher().resource("/prj/list").json())
{
- var cells = table.add_row([
- project.Id,
+ var row = table.add_row([
+ "",
project.ProjectRootDir,
project.EngineRootDir,
]);
- cells[0].link("", {"page" : "project", "project" : project.Id});
+ var cell = row.get_cell(0);
+ cell.tag().text(project.Id).on_click((x) => this.view_project(x), project.Id);
- var action_tb = new Toolbar(cells.at(-1), true);
- action_tb.left().add("view").on_click(() => void(0));
- action_tb.left().add("drop").on_click(() => this.on_drop());
+ 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);
}
// stats
section = this.add_section("stats");
- var providers = zen_fetch("/stats");
+ columns = [
+ "name",
+ "req count",
+ "size disk",
+ "size mem",
+ "cid total",
+ ];
+ const stats_table = new Table(section, columns);
+ var providers = new Fetcher().resource("stats").json();
for (var provider of (await providers)["providers"])
{
- var stats = zen_fetch("/stats/" + provider);
+ var stats = await new Fetcher().resource("stats", provider).json();
+ var values = [""];
+ try {
+ values.push(stats.requests.count);
+ const size_stat = (stats.store || stats.cache).size;
+ values.push(Friendly.kib(size_stat.disk));
+ values.push(Friendly.kib(size_stat.memory));
+ values.push(stats.cid.size.total);
+ }
+ catch {}
+ row = stats_table.add_row(values);
+ row.get_cell(0).tag().text(provider).on_click((x) => this.view_stat(x), provider);
+ }
+ }
- var section_provider = section.add_section(provider);
- var table = new PropTable(section_provider);
+ view_stat(provider)
+ {
+ window.location = "?page=stat&provider=" + provider;
+ }
- stats = zen_flatten(await stats);
- for (const key in stats)
- table.add_property(key, stats[key]);
+ view_project(project_id)
+ {
+ window.location = "?page=project&project=" + project_id;
+ }
+
+ drop_project(project_id)
+ {
+ const drop = async () => {
+ await new Fetcher().resource("prj", project_id).delete();
+ this.reload();
+ };
+
+ new Modal()
+ .title("Confirmation")
+ .message(`Drop project '${project_id}'?`)
+ .option("Yes", () => drop())
+ .option("No");
+ }
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+class Stat extends ZenPage
+{
+ static TemporalStat = class
+ {
+ constructor(data)
+ {
+ this._data = data;
}
+
+ toString()
+ {
+ const columns = [
+ [],
+ ["rate;"], [],
+ ["t;"], [], [],
+ ];
+ const data = this._data;
+ for (var key in data)
+ {
+ var value = key + ": " + Friendly.sep(data[key], 2);
+ value = value.replace("rate_", "");
+ value = value.replace("t_", "");
+
+ if (key.startsWith("rate_")) columns[2].push(value);
+ else if (key.startsWith("t_p")) columns[5].push(value);
+ else if (key.startsWith("t_")) columns[4].push(value);
+ else columns[0].push(Friendly.sep(data[key]));
+ }
+
+ var line_count = 0
+ for (var column of columns)
+ line_count = Math.max(line_count, column.length);
+
+ const widths = [13, 5, 19, 2, 23, -1];
+
+ var content = "";
+ for (var i = 0; i < line_count; ++i)
+ {
+ for (var j in columns)
+ {
+ const column = columns[j];
+ var cell = (column.length > i) ? column[i] : "";
+ var lead = cell.indexOf(":");
+ if (lead >= 0)
+ cell = " ".repeat(7 - lead) + cell;
+ if (widths[j] > 0)
+ cell = cell.padEnd(widths[j]);
+ content += cell;
+ }
+ content += "\n";
+ }
+
+ return "<pre>" + content + "</pre>";
+ }
+ }
+
+ async main()
+ {
+ const provider = this.get_param("provider", "z$");
+ var stats = new Fetcher()
+ .resource("stats", provider)
+ .param("cidstorestats", "true")
+ .param("cachestorestats", "true")
+ .json();
+
+ this.set_title("stat - " + provider);
+ const section = this.add_section(provider);
+
+ var toolbar = new Toolbar(section);
+ var tb_right = toolbar.right();
+ tb_right.add("filter:");
+ tb_right.add("-none-").on_click((x) => this.update_filter(""));
+ for (var preset of ["read.", "write.", ".request", ".bytes"])
+ tb_right.add(preset).on_click((x) => this.update_filter(x), preset);
+ {
+ var input = tb_right.add("", "input")
+ input.on("change", (x) => this.update_filter(x.inner().value), input);
+ }
+
+ this._table = new PropTable(section);
+
+ this._stats = stats = await stats;
+ this._condense(stats);
+
+ var first = undefined;
+ for (var name in stats)
+ {
+ first = first || name;
+ toolbar.left().add(name).on_click((x) => this.view_category(x), name);
+ }
+
+ first = this.get_param("view", first);
+ this.view_category(first);
+
+ var filter = this.get_param("filter");
+ if (filter)
+ this.update_filter(filter);
}
- on_drop()
+ view_category(name)
{
- new Modal().title("Confirmation").message("TODO").option("Okay");
+ const friendly = (this.get_param("raw") == undefined);
+ this._table.clear();
+ this._table.add_object(this._stats[name], friendly, 3);
+ this.set_param("view", name);
+ this.set_param("filter", "");
+ }
+
+ update_filter(needle)
+ {
+ this.set_param("filter", needle);
+ if (!needle)
+ return this._table.filter();
+
+ var needles = needle.split(" ");
+ this._table.filter(...needles);
+ }
+
+ _condense(stats)
+ {
+ const impl = function(node)
+ {
+ for (var name in node)
+ {
+ const candidate = node[name];
+ if (!(candidate instanceof Object))
+ continue;
+
+ if (candidate["rate_mean"] != undefined)
+ {
+ node[name] = new Stat.TemporalStat(candidate);
+ continue;
+ }
+
+ impl(candidate);
+ }
+ }
+
+ for (var name in stats)
+ impl(stats[name]);
}
}
+
+
////////////////////////////////////////////////////////////////////////////////
function add_branding(parent)
{
@@ -709,11 +1236,12 @@ async function main_guarded()
const page = params.get("page");
var impl = undefined;
- if (page == "project") impl = new Project(root, params);
- if (page == "oplog") impl = new Oplog(root, params);
- if (page == "entry") impl = new Entry(root, params);
- if (page == "test") impl = new Test(root, params);
- if (page == undefined) impl = new Start(root, params);
+ if (page == "project") impl = new Project(root, params);
+ else if (page == "stat") impl = new Stat(root, params);
+ else if (page == "oplog") impl = new Oplog(root, params);
+ else if (page == "entry") impl = new Entry(root, params);
+ else if (page == "test") impl = new Test(root, params);
+ else if (page == undefined) impl = new Start(root, params);
if (impl == undefined)
{