add original version of old crummy monitor
This commit is contained in:
parent
b00f8a357c
commit
ab7f6014a4
7 changed files with 368 additions and 114 deletions
|
|
@ -42,8 +42,7 @@
|
|||
<li class="player" data-cid="client ID">
|
||||
<span class="name">player name</span>
|
||||
<div class="admin-only">
|
||||
(<span class="points">points</span> pts)
|
||||
(<span class="tokens">tokens</span> tks)
|
||||
(<span class="points">points</span> pts) (<span class="tokens">tokens</span> tks)
|
||||
<button data-eval="send('control', {target: client.id, action: 'monitor'})">
|
||||
monitor
|
||||
</button>
|
||||
|
|
|
|||
129
public/buzzer.js
129
public/buzzer.js
|
|
@ -10,89 +10,27 @@ const storage = window.sessionStorage
|
|||
// - measure/report latency
|
||||
// - use server reported time to find winner
|
||||
|
||||
import config from "./config.js"
|
||||
import {
|
||||
clear,
|
||||
isEmpty,
|
||||
isString,
|
||||
ms_ns,
|
||||
node,
|
||||
off,
|
||||
on,
|
||||
q,
|
||||
s_ns,
|
||||
servertime_now_ns,
|
||||
session_id,
|
||||
session_id_from_url,
|
||||
session_url,
|
||||
} from "./shared.js"
|
||||
|
||||
const buzzer_key = {
|
||||
code: 0x20,
|
||||
name: "space bar",
|
||||
}
|
||||
|
||||
function isString(x) {
|
||||
return typeof x === "string"
|
||||
}
|
||||
|
||||
function isEmpty(x) {
|
||||
return (Array.isArray(x) ? x : Object.keys(x)).length === 0
|
||||
}
|
||||
|
||||
const q = (selector, root = document) => root.querySelector(selector)
|
||||
|
||||
const _evlisteners = {}
|
||||
/**
|
||||
* @param {String} event [description]
|
||||
* @param {Function} cb [description]
|
||||
* @param {?{once: bool, target: Element = document}} options
|
||||
*/
|
||||
function on(event, cb, options = {}) {
|
||||
const target = options.target || document
|
||||
if ((_evlisteners[target] ?? {})[event]) {
|
||||
console.warn(`Replacing previous event listener for '${event}' on target:`, target)
|
||||
off(event, target)
|
||||
}
|
||||
target.addEventListener(event, cb, { once: !!options.once })
|
||||
if (!_evlisteners[target]) {
|
||||
_evlisteners[target] = {}
|
||||
}
|
||||
_evlisteners[target][event] = cb
|
||||
}
|
||||
|
||||
function off(event, target = document) {
|
||||
if (!_evlisteners[target] || !_evlisteners[target][event]) {
|
||||
return
|
||||
}
|
||||
target.removeEventListener(event, _evlisteners[target][event])
|
||||
delete _evlisteners[target][event]
|
||||
if (isEmpty(_evlisteners[target])) {
|
||||
delete _evlisteners[target]
|
||||
}
|
||||
}
|
||||
|
||||
function node(type, { appendTo, cls, text, data, style, ...attrs } = {}) {
|
||||
let elem = isString(type) ? document.createElement(type) : type
|
||||
if (cls) {
|
||||
if (isString(cls)) {
|
||||
elem.className = cls
|
||||
} else {
|
||||
for (const [name, on] of Object.entries(cls)) {
|
||||
if (on) {
|
||||
elem.classList.add(name)
|
||||
} else {
|
||||
elem.classList.remove(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
elem.textContent = text
|
||||
}
|
||||
Object.assign(elem.dataset, data ?? {})
|
||||
Object.assign(elem.style, style ?? {})
|
||||
for (const name in attrs) {
|
||||
elem.setAttribute(name, attrs[name])
|
||||
}
|
||||
if (appendTo) {
|
||||
elem = appendTo.appendChild(elem)
|
||||
}
|
||||
return elem
|
||||
}
|
||||
|
||||
/**
|
||||
* Some duration conversion constants.
|
||||
*/
|
||||
const ms_ns = 1_000_000 // nanoseconds in a millisecond
|
||||
const s_ms = 1_000 // milliseconds in a second
|
||||
const s_ns = 1_000_000_000 // nanoseconds in a second
|
||||
|
||||
let socket,
|
||||
servertime,
|
||||
toffset_ms,
|
||||
|
|
@ -114,11 +52,6 @@ function find_client(client_id) {
|
|||
return clients.find((c) => c.id === client_id)
|
||||
}
|
||||
|
||||
function session_id() {
|
||||
const match = /^#?(.+)/.exec(document.location.hash)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
function new_session_id() {
|
||||
if (!crypto) {
|
||||
return Math.random().toString(36).substr(2)
|
||||
|
|
@ -130,7 +63,7 @@ function new_session_id() {
|
|||
|
||||
function setup_url() {
|
||||
const sid = session_id() || new_session_id()
|
||||
document.location.hash = sid
|
||||
location.hash = sid
|
||||
}
|
||||
|
||||
function send(type, value) {
|
||||
|
|
@ -138,13 +71,6 @@ function send(type, value) {
|
|||
socket.send(JSON.stringify({ type, value }))
|
||||
}
|
||||
|
||||
function clear(container) {
|
||||
while (container.children.length > 0) {
|
||||
const child = container.children[0]
|
||||
child.remove()
|
||||
}
|
||||
}
|
||||
|
||||
const player_list = q("#info ul")
|
||||
function redraw_clients(me, clients) {
|
||||
if (!me) {
|
||||
|
|
@ -196,15 +122,6 @@ function highlight(client_id, until_ns) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess the exact current server time.
|
||||
*/
|
||||
function servertime_now_ns() {
|
||||
const now_ms = performance.now()
|
||||
const delta_ns = ms_ns * (now_ms - toffset_ms)
|
||||
return servertime + delta_ns
|
||||
}
|
||||
|
||||
function setup_ui() {
|
||||
on("focus", (event) => {
|
||||
hide("active")
|
||||
|
|
@ -306,20 +223,10 @@ function set_name(name) {
|
|||
send("name", name)
|
||||
}
|
||||
|
||||
function session_url(sid) {
|
||||
return `${config.wsurl}/${sid}`
|
||||
}
|
||||
|
||||
function session_id_from_url(url) {
|
||||
const wsurl = new URL(config.wsurl)
|
||||
const match = RegExp(`${wsurl.pathname}/([^/]+)$`).exec(url)
|
||||
return !match ? null : match[1]
|
||||
}
|
||||
|
||||
function setup_ws() {
|
||||
const sid = session_id()
|
||||
const credentials = { id: storage["my_uid"], key: storage["my_key"] }
|
||||
socket = new WebSocket(`${session_url(sid)}`)
|
||||
socket = new WebSocket(session_url(sid))
|
||||
socket.addEventListener("open", function (event) {
|
||||
if (sid === storage["my_sid"]) {
|
||||
send("login", credentials)
|
||||
|
|
@ -362,6 +269,8 @@ function setup_ws() {
|
|||
clients.push(client)
|
||||
}
|
||||
redraw_clients(me, clients)
|
||||
} else if (type === "control") {
|
||||
// ignore
|
||||
} else if (type === "error") {
|
||||
console.error(`Error: ${value.reason}`)
|
||||
const errorbox = q("#error")
|
||||
|
|
|
|||
60
public/monitor.css
Normal file
60
public/monitor.css
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
body {
|
||||
font-family: "Arial Rounded MT Bold", sans-serif;
|
||||
background-color: black;
|
||||
margin: 0px auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/*
|
||||
# colors
|
||||
citrus: c2d72f
|
||||
bleue: 0093ff
|
||||
*/
|
||||
#info {
|
||||
background-color: #c2d72f;
|
||||
padding: 1em 0 0 1em;
|
||||
width: 1280px;
|
||||
height: 720px;
|
||||
outline: 10px solid gold;
|
||||
}
|
||||
.player {
|
||||
width: 336px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
.player + .player {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
.box {
|
||||
background-color: #0f0;
|
||||
height: 189px;
|
||||
outline: 0.3em solid grey;
|
||||
-moz-outline-radius: 1em;
|
||||
}
|
||||
.points {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
background-color: #c2d72f;
|
||||
width: 3em;
|
||||
padding: 0.2em;
|
||||
text-align: right;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 0 0 0.3em #c2d72f, 0 0 0.8em #c2d72f, 0 0 1em #c2d72f, 0 0 1.2em #c2d72f;
|
||||
}
|
||||
.player.buzzing .box {
|
||||
outline: 0.7em solid #0093ff;
|
||||
}
|
||||
.name {
|
||||
margin-top: 0.5em;
|
||||
text-align: center;
|
||||
letter-spacing: 0.3em;
|
||||
font-variant: small-caps;
|
||||
/*font-weight: bold;*/
|
||||
}
|
||||
/*1280 x 720*/
|
||||
/*1920 x 1080*/
|
||||
24
public/monitor.html
Normal file
24
public/monitor.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" type="text/css" href="monitor.css" />
|
||||
|
||||
<body>
|
||||
<div id="info">
|
||||
<div class="player">
|
||||
<div class="points">-36'000</div>
|
||||
<div class="box"></div>
|
||||
<div class="name">Player 1</div>
|
||||
</div>
|
||||
<div class="player buzzing">
|
||||
<div class="points">0</div>
|
||||
<div class="box"></div>
|
||||
<div class="name">Player 2</div>
|
||||
</div>
|
||||
<div class="player">
|
||||
<div class="points">900</div>
|
||||
<div class="box"></div>
|
||||
<div class="name">Player 3</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script type="module" src="./monitor.js"></script>
|
||||
149
public/monitor.js
Normal file
149
public/monitor.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"use strict"
|
||||
|
||||
/* global document, window */
|
||||
const location = document.location
|
||||
const performance = window.performance
|
||||
|
||||
import {
|
||||
clear,
|
||||
ms_ns,
|
||||
node,
|
||||
q,
|
||||
s_ns,
|
||||
servertime_now_ns,
|
||||
session_id,
|
||||
session_url,
|
||||
} from "./shared.js"
|
||||
|
||||
let socket,
|
||||
servertime,
|
||||
toffset_ms,
|
||||
clients = {},
|
||||
me
|
||||
|
||||
function setup_url() {
|
||||
const sid = session_id() || ""
|
||||
location.hash = sid
|
||||
}
|
||||
|
||||
function send(type, value) {
|
||||
// console.debug('sending', value)
|
||||
socket.send(JSON.stringify({ type, value }))
|
||||
}
|
||||
|
||||
function prettynum(n) {
|
||||
let i = Math.abs(n) | 0
|
||||
let s = []
|
||||
while (true) {
|
||||
const m = i % 1000
|
||||
i = (i / 1000) | 0
|
||||
if (i === 0) {
|
||||
s.unshift(String(m))
|
||||
break
|
||||
} else {
|
||||
s.unshift(String(m).padStart(3, "0"))
|
||||
}
|
||||
}
|
||||
return (n < 0 ? "-" : "") + s.join("'")
|
||||
}
|
||||
|
||||
function assert(expected, got) {
|
||||
if (expected !== got) {
|
||||
throw Error("Assertion failed.")
|
||||
}
|
||||
}
|
||||
|
||||
function test_prettynum() {
|
||||
assert("0", prettynum(0))
|
||||
assert("1", prettynum(1))
|
||||
assert("1'000", prettynum(1000))
|
||||
assert("36'000", prettynum(36000))
|
||||
assert("0", prettynum(-0))
|
||||
assert("-1", prettynum(-1))
|
||||
assert("-1'000", prettynum(-1000))
|
||||
assert("-36'000", prettynum(-36000))
|
||||
assert("-36'600", prettynum(-36600))
|
||||
}
|
||||
test_prettynum()
|
||||
|
||||
const player_list = q("#info")
|
||||
function redraw_clients(me, clients) {
|
||||
if (!me) {
|
||||
return
|
||||
}
|
||||
clear(player_list)
|
||||
for (const c of Object.values(clients)) {
|
||||
if (c.id === me.id) {
|
||||
continue
|
||||
}
|
||||
const player = node("div", { data: { cid: c.id }, cls: "player" })
|
||||
node("div", { cls: "points", text: prettynum(c.points), appendTo: player })
|
||||
node("div", { cls: "box", appendTo: player })
|
||||
node("div", { cls: "name", text: c.name, appendTo: player })
|
||||
player_list.appendChild(player)
|
||||
}
|
||||
}
|
||||
|
||||
let highlighted = false
|
||||
function highlight(client_id, until_ns) {
|
||||
if (highlighted) {
|
||||
return
|
||||
}
|
||||
const timeout_ms = (until_ns - servertime_now_ns()) / ms_ns
|
||||
if (timeout_ms <= 10) {
|
||||
console.warn("That highlight timeout was ridiculously low:", client_id, timeout_ms)
|
||||
return
|
||||
}
|
||||
for (const li of player_list.children) {
|
||||
if (li.dataset.cid === client_id) {
|
||||
li.classList.add("buzzing")
|
||||
highlighted = true
|
||||
setTimeout(() => {
|
||||
highlighted = false
|
||||
li.classList.remove("buzzing")
|
||||
}, timeout_ms)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setup_ws() {
|
||||
const sid = session_id()
|
||||
socket = new WebSocket(session_url(sid))
|
||||
socket.addEventListener("open", function (event) {
|
||||
send("name", "Monitor")
|
||||
})
|
||||
socket.addEventListener("message", function (event) {
|
||||
const msg = JSON.parse(event.data)
|
||||
const { type, value } = msg
|
||||
if (msg.type === "time") {
|
||||
servertime = value.time
|
||||
toffset_ms = performance.now()
|
||||
} else if (msg.type === "id") {
|
||||
me = value
|
||||
redraw_clients(me, clients)
|
||||
} else if (msg.type === "buzz") {
|
||||
const buzztime_ns = value.time
|
||||
const client_id = value.client
|
||||
const duration_ns = 12 * s_ns
|
||||
const until_ns = buzztime_ns + duration_ns
|
||||
highlight(client_id, until_ns)
|
||||
} else if (msg.type === "clients") {
|
||||
clients = Object.fromEntries(value.clients.map((c) => [c.id, c]))
|
||||
redraw_clients(me, clients)
|
||||
} else if (msg.type === "client") {
|
||||
const client = value.client
|
||||
clients[client.id] = client
|
||||
redraw_clients(me, clients)
|
||||
} else if (type === "session_key") {
|
||||
// ignore
|
||||
} else if (type === "error") {
|
||||
console.error(`Error: ${value.reason}`)
|
||||
} else {
|
||||
console.error(`Unknown message: ${event.data}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setup_url()
|
||||
setup_ws()
|
||||
115
public/shared.js
Normal file
115
public/shared.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"use strict"
|
||||
|
||||
/* global document, window */
|
||||
const location = document.location
|
||||
const performance = window.performance
|
||||
|
||||
import config from "./config.js"
|
||||
|
||||
export function isString(x) {
|
||||
return typeof x === "string"
|
||||
}
|
||||
|
||||
export function isEmpty(x) {
|
||||
return (Array.isArray(x) ? x : Object.keys(x)).length === 0
|
||||
}
|
||||
|
||||
export const q = (selector, root = document) => root.querySelector(selector)
|
||||
|
||||
const _evlisteners = {}
|
||||
|
||||
/**
|
||||
* @param {String} event [description]
|
||||
* @param {Function} cb [description]
|
||||
* @param {?{once: bool, target: Element = document}} options
|
||||
*/
|
||||
export function on(event, cb, options = {}) {
|
||||
const target = options.target || document
|
||||
if ((_evlisteners[target] ?? {})[event]) {
|
||||
console.warn(`Replacing previous event listener for '${event}' on target:`, target)
|
||||
off(event, target)
|
||||
}
|
||||
target.addEventListener(event, cb, { once: !!options.once })
|
||||
if (!_evlisteners[target]) {
|
||||
_evlisteners[target] = {}
|
||||
}
|
||||
_evlisteners[target][event] = cb
|
||||
}
|
||||
|
||||
export function off(event, target = document) {
|
||||
if (!_evlisteners[target] || !_evlisteners[target][event]) {
|
||||
return
|
||||
}
|
||||
target.removeEventListener(event, _evlisteners[target][event])
|
||||
delete _evlisteners[target][event]
|
||||
if (isEmpty(_evlisteners[target])) {
|
||||
delete _evlisteners[target]
|
||||
}
|
||||
}
|
||||
|
||||
export function node(type, { appendTo, cls, text, data, style, ...attrs } = {}) {
|
||||
let elem = isString(type) ? document.createElement(type) : type
|
||||
if (cls) {
|
||||
if (isString(cls)) {
|
||||
elem.className = cls
|
||||
} else {
|
||||
for (const [name, on] of Object.entries(cls)) {
|
||||
if (on) {
|
||||
elem.classList.add(name)
|
||||
} else {
|
||||
elem.classList.remove(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
elem.textContent = text
|
||||
}
|
||||
Object.assign(elem.dataset, data ?? {})
|
||||
Object.assign(elem.style, style ?? {})
|
||||
for (const name in attrs) {
|
||||
elem.setAttribute(name, attrs[name])
|
||||
}
|
||||
if (appendTo) {
|
||||
elem = appendTo.appendChild(elem)
|
||||
}
|
||||
return elem
|
||||
}
|
||||
|
||||
/**
|
||||
* Some duration conversion constants.
|
||||
*/
|
||||
export const ms_ns = 1_000_000 // nanoseconds in a millisecond
|
||||
export const s_ms = 1_000 // milliseconds in a second
|
||||
export const s_ns = 1_000_000_000 // nanoseconds in a second
|
||||
|
||||
export function session_url(sid) {
|
||||
return `${config.wsurl}/${sid}`
|
||||
}
|
||||
|
||||
export function session_id_from_url(url) {
|
||||
const wsurl = new URL(config.wsurl)
|
||||
const match = RegExp(`${wsurl.pathname}/([^/]+)$`).exec(url)
|
||||
return !match ? null : match[1]
|
||||
}
|
||||
|
||||
export function clear(container) {
|
||||
while (container.children.length > 0) {
|
||||
const child = container.children[0]
|
||||
child.remove()
|
||||
}
|
||||
}
|
||||
|
||||
export function session_id() {
|
||||
const match = /^#?(.+)/.exec(location.hash)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess the exact current server time.
|
||||
*/
|
||||
export function servertime_now_ns() {
|
||||
const now_ms = performance.now()
|
||||
const delta_ns = ms_ns * (now_ms - toffset_ms)
|
||||
return servertime + delta_ns
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue