diff --git a/public/assets/pages/adminpage/ctrl_activity_graph.css b/public/assets/pages/adminpage/ctrl_activity_graph.css
index 52f41f08..ff3bb15d 100644
--- a/public/assets/pages/adminpage/ctrl_activity_graph.css
+++ b/public/assets/pages/adminpage/ctrl_activity_graph.css
@@ -1,23 +1,29 @@
-.component_stats {
+.component_page_admin .component_stats {
clear: both;
+ height: 100px;
+ z-index: 1;
+ margin-bottom: 30px;
+}
+.component_page_admin .component_logviewer {
+ z-index: 0;
}
.component_stats .chart {
display: flex;
align-items: flex-end;
- height: 100px;
+ justify-content: end;
+ height: 100%;
}
.component_stats .chart .bar {
+ max-width: 45px;
+ cursor: pointer;
display: flex;
flex: 1;
- cursor: pointer;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
- border-bottom: 1px solid var(--light);
-
- background: var(--border);
+ background: #ebebec;
border: 2px solid var(--border);
}
-.component_stats .chart .bar[title="0"], .component_stats .chart .bar[title="1"], .component_stats .chart .bar[title="2"] {
+.component_stats .chart .bar[title="0"], .component_stats .chart .bar[title="1"] {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
@@ -27,3 +33,12 @@
color: var(--light);
font-size: 0.8rem;
}
+.component_stats .legend .title {
+ font-style: italic;
+}
+.component_stats .legend.invisible {
+ opacity: 0;
+}
+.component_stats .component_skeleton {
+ margin-bottom: 5px;
+}
diff --git a/public/assets/pages/adminpage/ctrl_activity_graph.js b/public/assets/pages/adminpage/ctrl_activity_graph.js
index 684669e9..04b7bb80 100644
--- a/public/assets/pages/adminpage/ctrl_activity_graph.js
+++ b/public/assets/pages/adminpage/ctrl_activity_graph.js
@@ -1,48 +1,83 @@
import { createElement } from "../../lib/skeleton/index.js";
import rxjs, { effect } from "../../lib/rx.js";
-import { get as getLogs } from "./model_log.js";
+import { generateSkeleton } from "../../components/skeleton.js";
import { loadCSS } from "../../helpers/loader.js";
+import { get as getLogs } from "./model_log.js";
+
+const NUMBER_BUCKETS = 30;
+const MIN_TIME_WIDTH = 5000;
+
export default async function(render) {
+ render(createElement(`
${generateSkeleton(3)}
`));
await loadCSS(import.meta.url, "./ctrl_activity_graph.css");
- effect(getLogs().pipe(
- rxjs.map((log) => {
- const times = log.trim().split("\n").map((line) => new Date(line.substring(0, 19)).getTime());
- const start = times[0];
- const end = times[times.length - 1];
-
- const size = 30
- const bars = Array(size).fill(0);
- const width = (end - start) / size;
- for (const t of times) {
- const idx = Math.min(size - 1, Math.max(0, Math.floor((t - start) / width)));
- bars[idx] += 1;
+ effect(getLogs(300).pipe(
+ rxjs.first(),
+ rxjs.repeat({ delay: 2500 }),
+ rxjs.scan(({ start, end, width, max, init = true, buckets = Array(NUMBER_BUCKETS).fill(0) }, logfile) => {
+ const times = logfile.trim().split("\n").map((line) => new Date(line.substring(0, 19)).getTime());
+ if (init === true) {
+ start = times[0];
+ end = times[times.length - 1];
+ width = Math.max((end - start) / NUMBER_BUCKETS, MIN_TIME_WIDTH);
+ for (let i=times.length-1; i>=0; i--) {
+ let idx = Math.floor((times[i] - start) / width);
+ if (idx === NUMBER_BUCKETS) idx -= 1;
+ buckets[idx] += 1;
+ }
+ for (let i=buckets.length-1; i>=0; i--) {
+ if (buckets[i] === 0) buckets[i] = -1;
+ else break;
+ }
+ max = Math.max(1, ...buckets);
+ init = false;
+ } else {
+ for (let i=times.length-1; i>=0; i--) {
+ const current = times[i];
+ // start end current
+ // | | |
+ // |=============|<-- new -->
+ if (current <= end) {
+ break;
+ }
+ const idx = Math.floor((times[i] - start) / width);
+ if (!buckets[idx]) buckets[idx] = 0;
+ buckets[idx] += 1;
+ }
+ const shift = buckets.length - NUMBER_BUCKETS;
+ for (let i=0; i {
- const max = Math.max(1, ...bars);
+ return { start, end, width, buckets, max, init };
+ }, {}),
+ rxjs.tap(({ buckets, start, end, max }) => {
const $root = document.createDocumentFragment();
const $chart = createElement(``);
- for (let i = 0; i < bars.length; i++) {
- const $bar = createElement(``)
- $bar.style.height = Math.sqrt(bars[i]) / Math.sqrt(max) * 100 + "%";
+ let display = true;
+ for (let i = 0; i < buckets.length; i++) {
+ if (buckets[i] < 0) {
+ display = false;
+ continue;
+ }
+ const $bar = createElement(``);
+ const height = Math.sqrt(buckets[i]) / Math.sqrt(max) * 100;
+ $bar.style.height = Math.min(height, 120) + "%";
$chart.appendChild($bar);
}
$root.appendChild($chart);
$root.appendChild(createElement(`
- ${start}
- ${end}
+ ${new Date(start).toLocaleTimeString()}
+ Log Events
+ ${new Date(end).toLocaleTimeString()}
`));
- render($root);
+ if (display) render($root);
}),
- rxjs.catchError(() => rxjs.EMPTY),
+ rxjs.catchError((err) => rxjs.EMPTY),
));
}
diff --git a/public/assets/pages/adminpage/ctrl_activity_viewer.js b/public/assets/pages/adminpage/ctrl_activity_viewer.js
index 25abe402..089ea378 100644
--- a/public/assets/pages/adminpage/ctrl_activity_viewer.js
+++ b/public/assets/pages/adminpage/ctrl_activity_viewer.js
@@ -19,13 +19,15 @@ export default async function(render) {
const $log = qs($page, "pre");
render($page);
- effect(getLogs().pipe(
+ effect(rxjs.of(null).pipe(
+ rxjs.mergeMap(() => $log.matches(":hover") ? rxjs.EMPTY : getLogs()),
rxjs.map((logData) => logData + "\n\n\n\n\n"),
stateMutation($log, "textContent"),
rxjs.tap(() => {
if ($log?.scrollTop !== 0) return;
$log.scrollTop = $log.scrollHeight;
}),
+ rxjs.repeat({ delay: 2500 }),
rxjs.catchError(() => rxjs.EMPTY),
));
}
diff --git a/public/assets/pages/adminpage/ctrl_setup.js b/public/assets/pages/adminpage/ctrl_setup.js
index 6d86f894..d1357208 100644
--- a/public/assets/pages/adminpage/ctrl_setup.js
+++ b/public/assets/pages/adminpage/ctrl_setup.js
@@ -54,7 +54,7 @@ function setupHOC(ctrlWrapped) {
function componentStep1(render) {
const $page = createElement(`
-
Welcome Aboard, Captain!
+
Welcome Aboard!
First thing first, setup your password: