From eaa730d2895f57d9745ea2474e02d194ea16f48d Mon Sep 17 00:00:00 2001
From: Paul Oliver <contact@pauloliver.dev>
Date: Sat, 21 Mar 2026 01:36:24 +0100
Subject: Adds data server (WIP)

---
 data/vue/App.vue | 242 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 242 insertions(+)
 create mode 100644 data/vue/App.vue

(limited to 'data/vue/App.vue')

diff --git a/data/vue/App.vue b/data/vue/App.vue
new file mode 100644
index 0000000..114857a
--- /dev/null
+++ b/data/vue/App.vue
@@ -0,0 +1,242 @@
+<template>
+  <div ref="top_pad"></div>
+  <div class="top_bar" ref="top_bar">
+    <h1>
+      Salis data server »
+      <span class="opts_name">
+        {{ opts.name }}
+        {{ query_in_progress ? '⧖' : '✓' }}
+      </span>
+    </h1>
+    <form @change="on_form_change">
+      <span class="nobr">Entries (max): <input class="input_small" v-model="entries" /></span><wbr />
+      <span class="nobr">nth: <input class="input_small" v-model="nth" /></span><wbr />
+      <span class="nobr">X-axis: <select class="input_small" v-model="x_axis"><option v-for="axis in x_axes">{{ axis }}</option></select></span><wbr />
+      <span class="nobr">X-low: <input v-model="x_low" /></span><wbr />
+      <span class="nobr">X-high: <input v-model="x_high" /></span>
+    </form>
+  </div>
+  <Section name="Options">
+    <table>
+      <tr v-for="opt_fmt in opt_fmts">
+        <td>{{ opt_fmt[0] }}:</td>
+        <td>{{ opt_fmt[2](opts[opt_fmt[1]]) }}</td>
+      </tr>
+    </table>
+  </Section>
+  <!-- Render plots after simulation options have been loaded -->
+  <div v-if="loaded">
+    <Section :name="section" grid v-for="(section_plots, section) in plots">
+      <Plot :name="name" :section="section" v-for="(_, name) in section_plots" />
+    </Section>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, provide, useTemplateRef, ref } from 'vue'
+
+import Plot from './Plot.vue'
+import Section from './Section.vue'
+
+const root = window.location.href
+const id = v => v
+const hex = v => v !== undefined ? `0x${v.toString(16)}` : ''
+const hex_pow = v => v !== undefined ? `0x${Math.pow(2, v).toString(16)}` : ''
+const disabled = v => v ? 'disabled' : 'enabled'
+
+const opt_fmts = [
+  ['Ancestor', 'anc', id],
+  ['Architecture', 'arch', id],
+  ['Auto-save interval', 'auto_save_pow', hex_pow],
+  ['Clones', 'clones', id],
+  ['Cores', 'cores', id],
+  ['Data push interval', 'data_push_pow', hex_pow],
+  ['Mutator flip bit', 'muta_flip', id],
+  ['Mutator range', 'muta_pow', hex_pow],
+  ['Memory vector size', 'mvec_pow', hex_pow],
+  ['Save file compression', 'no_compress', disabled],
+  ['Seed', 'seed', hex],
+]
+
+const opts = ref({})
+const plots = ref({})
+const tables = ref({})
+const loaded = ref(false)
+
+const entries = ref(2000)
+const nth = ref(BigInt(1))
+const x_axes = ref(['rowid'])
+const x_axis = ref(x_axes.value[0])
+const x_low = ref(hex(BigInt(0)))
+const x_high = ref(hex(BigInt(Math.pow(2, 64))))
+
+const plot_x_low = ref(0n)
+const plot_redraw = ref(false)
+const query_in_progress = ref(false)
+const data = ref([])
+
+const top_pad = useTemplateRef('top_pad')
+const top_bar = useTemplateRef('top_bar')
+
+provide('plots', plots)
+provide('entries', entries)
+provide('x_axis', x_axis)
+provide('data', data)
+
+const sanitize = (input, min, max, def, fmt) => {
+  if (isNaN(Number(input.value)) || input.value === '' || input.value < min || input.value > max) {
+    input.value = fmt(def)
+  }
+}
+
+const on_form_change = () => {
+  sanitize(entries, 1n, BigInt(Math.pow(2, 64)), 2000n, id)
+  sanitize(nth, 1n, BigInt(Math.pow(2, 64)), 1n, id)
+  sanitize(x_low, 0n, BigInt(Math.pow(2, 64)), 0n, hex)
+  sanitize(x_high, 1n, BigInt(Math.pow(2, 64)), BigInt(Math.pow(2, 64)), hex)
+
+  plot_x_low.value = x_low.value
+  plot_redraw.value = true
+
+  query(false)
+}
+
+const pad_top_bar = () => {
+  top_pad.value.style.height = `${Math.round(top_bar.value.getBoundingClientRect().height)}px`
+}
+
+const reviver = (_, val, { source }) => {
+  if (Number.isInteger(val) && !Number.isSafeInteger(val)) {
+    try { return BigInt(source) } catch {}
+  }
+
+  return val
+}
+
+const query_table = async table => {
+  const params = {
+    table: table,
+    entries: entries.value,
+    nth: nth.value,
+    x_axis: x_axis.value,
+    x_low: Number(plot_x_low.value),
+    x_high: Number(x_high.value),
+  }
+
+  const search_params = new URLSearchParams(params)
+  const resp_table = await fetch(root + `data?${search_params}`, { method: 'GET' })
+  const resp_json = JSON.parse(await resp_table.text(), reviver)
+
+  // Keep track of the highest x-axis value fetched so far.
+  // Future queries will set this as the minimum, which prevents re-fetching already stored data.
+  if (resp_json.length) {
+    const x_last = BigInt(resp_json.slice(-1)[0][x_axis.value] + 1)
+    plot_x_low.value = plot_x_low.value > x_last ? plot_x_low.value : x_last
+  }
+
+  return resp_json
+}
+
+const query = async (reschedule = true) => {
+  if (query_in_progress.value) return
+
+  query_in_progress.value = true
+
+  const query_results = await Promise.all(tables.value.map(query_table))
+  const query_values = Object.fromEntries(tables.value.map((key, i) => [key, query_results[i]]))
+
+  data.value = { redraw: plot_redraw.value, values: query_values }
+  plot_redraw.value = false
+  query_in_progress.value = false
+
+  if (reschedule) setTimeout(query, 10000)
+}
+
+onMounted(async () => {
+  window.onresize = _ => pad_top_bar()
+  pad_top_bar()
+
+  // Fetch initial data
+  const resp_opts = await fetch(root + 'opts', { method: 'GET' })
+  const resp_plots = await fetch(root + 'plots', { method: 'GET' })
+  const resp_tables = await fetch(root + 'tables', { method: 'GET' })
+
+  opts.value = JSON.parse(await resp_opts.text(), reviver)
+  plots.value = JSON.parse(await resp_plots.text(), reviver)
+  tables.value = JSON.parse(await resp_tables.text(), reviver)
+  loaded.value = true
+
+  // All tables should include one cycle column for each core.
+  // This allows normalizing the plots against each core's cycle count
+  // (i.e. making `cycl_#` the plots' x-axis).
+  x_axes.value.push(...Array(opts.value.cores).keys().map(i => `cycl_${i}`))
+
+  query()
+})
+</script>
+
+<style>
+html {
+  background-color: #002b36;
+  color: #586e75;
+  font-family: sans-serif;
+}
+
+h1 {
+  font-size: 20px;
+  font-weight: 600;
+}
+
+input, select {
+  background-color: #586e75;
+  border: none;
+  color: #002b36;
+  font-family: monospace;
+  font-size: 14px;
+  margin: 0 4px;
+  padding: 2px;
+}
+
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+  height: 100%;
+  width: 100%;
+}
+
+tr:nth-child(odd) {
+  background-color: #073642;
+}
+
+td {
+  font-family: monospace;
+  font-size: 14px;
+  margin: 0;
+  padding: 0;
+}
+
+.top_bar {
+  background-color: #073642;
+  left: 0;
+  padding: 8px;
+  position: fixed;
+  top: 0;
+  width: 100%;
+  z-index: 1;
+}
+
+.opts_name {
+  color: #b58900;
+  font-weight: normal;
+}
+
+.nobr {
+  line-height: 32px;
+  margin-right: 16px;
+  white-space: nowrap;
+}
+
+.input_small {
+  width: 80px;
+}
+</style>
-- 
cgit v1.2.3-70-g09d2

