- html/css split into tokens.css (custom properties), base.css (reset, typography, focus, skip link), components.css (buttons, inputs, tables, status cards, popups, menu, log lines) and layout.css (shell, sidebar, header, breakpoints); screen.css removed. Selectors and declarations moved as-is; three pairs of conflicting rules resolved in favour of the one that already won the cascade, two empty rules dropped. - Every literal colour outside tokens.css is now a token (category badge borders excepted). color-scheme set so native controls follow. - Light theme under prefers-color-scheme: light, with explicit data-theme="light"/"dark" overrides on :root for a future toggle. Body text 13.6:1, muted text 5.4:1, accent 5.1:1 on the light background. - HTML: lang="en" everywhere, one h1 per page, maintenance page uses <main>, an id typo fixed.
This commit is contained in:
+305
@@ -0,0 +1,305 @@
|
||||
// Websocket transport between the web UI and xTeVe.
|
||||
//
|
||||
// One socket stays open for the life of the page. Requests are queued and
|
||||
// sent one at a time; each carries an id that the server echoes back, and
|
||||
// the response is matched on it (or, for older servers that close after a
|
||||
// single reply, to the oldest in-flight request). Connection loss triggers
|
||||
// a reconnect with exponential backoff, and queued requests survive it.
|
||||
|
||||
interface PendingRequest {
|
||||
id: string
|
||||
payload: Object
|
||||
resolve: (response: any) => void
|
||||
reject: (err: Error) => void
|
||||
timer: number
|
||||
sent: boolean
|
||||
}
|
||||
|
||||
class WSClient {
|
||||
private socket: WebSocket = null
|
||||
private queue: PendingRequest[] = []
|
||||
private inFlight: PendingRequest = null
|
||||
private nextID: number = 1
|
||||
private reconnectDelay: number = 500
|
||||
private reconnectTimer: number = 0
|
||||
private closedByUs: boolean = false
|
||||
|
||||
readonly requestTimeoutMs: number = 12000
|
||||
|
||||
private url(): string {
|
||||
var protocol = window.location.protocol == "https:" ? "wss://" : "ws://"
|
||||
var host = window.location.host
|
||||
if (host == undefined || host.length < 1) {
|
||||
host = window.location.hostname
|
||||
}
|
||||
return protocol + host + "/data/"
|
||||
}
|
||||
|
||||
// send queues a command and resolves with the parsed response.
|
||||
send(payload: Object): Promise<any> {
|
||||
var client = this
|
||||
return new Promise(function(resolve, reject) {
|
||||
var req: PendingRequest = {
|
||||
id: String(client.nextID++),
|
||||
payload: payload,
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
timer: 0,
|
||||
sent: false
|
||||
}
|
||||
client.queue.push(req)
|
||||
client.pump()
|
||||
})
|
||||
}
|
||||
|
||||
// hasQueued reports whether a command of this kind is already waiting.
|
||||
hasQueued(cmd: string): boolean {
|
||||
if (this.inFlight != null && this.inFlight.payload["cmd"] == cmd) {
|
||||
return true
|
||||
}
|
||||
for (var i = 0; i < this.queue.length; i++) {
|
||||
if (this.queue[i].payload["cmd"] == cmd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private connect(): void {
|
||||
if (this.socket != null && (this.socket.readyState == WebSocket.OPEN || this.socket.readyState == WebSocket.CONNECTING)) {
|
||||
return
|
||||
}
|
||||
|
||||
var client = this
|
||||
var socket = new WebSocket(this.url())
|
||||
this.socket = socket
|
||||
|
||||
socket.onopen = function() {
|
||||
WS_AVAILABLE = true
|
||||
client.reconnectDelay = 500
|
||||
client.pump()
|
||||
}
|
||||
|
||||
socket.onmessage = function(e: MessageEvent) {
|
||||
client.handleMessage(e.data)
|
||||
}
|
||||
|
||||
socket.onerror = function() {
|
||||
// onclose follows; nothing to do here.
|
||||
}
|
||||
|
||||
socket.onclose = function() {
|
||||
if (client.socket == socket) {
|
||||
client.socket = null
|
||||
}
|
||||
// An in-flight request that never got its answer goes back to the
|
||||
// front of the queue and is retried on the next connection.
|
||||
if (client.inFlight != null) {
|
||||
var req = client.inFlight
|
||||
client.inFlight = null
|
||||
req.sent = false
|
||||
client.queue.unshift(req)
|
||||
}
|
||||
client.scheduleReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.closedByUs || this.reconnectTimer != 0) {
|
||||
return
|
||||
}
|
||||
if (this.queue.length == 0) {
|
||||
// Idle: reconnect lazily on the next request.
|
||||
return
|
||||
}
|
||||
var client = this
|
||||
this.reconnectTimer = window.setTimeout(function() {
|
||||
client.reconnectTimer = 0
|
||||
client.connect()
|
||||
}, this.reconnectDelay)
|
||||
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 10000)
|
||||
}
|
||||
|
||||
// pump sends the next queued request if the socket is open and idle.
|
||||
private pump(): void {
|
||||
if (this.inFlight != null || this.queue.length == 0) {
|
||||
return
|
||||
}
|
||||
if (this.socket == null || this.socket.readyState != WebSocket.OPEN) {
|
||||
this.connect()
|
||||
return
|
||||
}
|
||||
|
||||
var req = this.queue.shift()
|
||||
this.inFlight = req
|
||||
req.sent = true
|
||||
req.payload["id"] = req.id
|
||||
|
||||
var client = this
|
||||
req.timer = window.setTimeout(function() {
|
||||
if (client.inFlight == req) {
|
||||
client.inFlight = null
|
||||
req.reject(new Error("timeout"))
|
||||
// A socket that stopped answering is not trusted any more.
|
||||
client.resetSocket()
|
||||
client.pump()
|
||||
}
|
||||
}, this.requestTimeoutMs)
|
||||
|
||||
try {
|
||||
this.socket.send(JSON.stringify(req.payload))
|
||||
} catch (err) {
|
||||
window.clearTimeout(req.timer)
|
||||
this.inFlight = null
|
||||
req.sent = false
|
||||
this.queue.unshift(req)
|
||||
this.resetSocket()
|
||||
}
|
||||
}
|
||||
|
||||
private resetSocket(): void {
|
||||
var socket = this.socket
|
||||
this.socket = null
|
||||
if (socket != null) {
|
||||
socket.onclose = null
|
||||
try { socket.close() } catch (err) { /* ignore */ }
|
||||
}
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
|
||||
private handleMessage(raw: string): void {
|
||||
var response: any
|
||||
try {
|
||||
response = JSON.parse(raw)
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
|
||||
var req = this.inFlight
|
||||
if (req == null) {
|
||||
return
|
||||
}
|
||||
// Servers that echo the id let us drop stray late answers.
|
||||
if (response.hasOwnProperty("id") && response["id"] != req.id) {
|
||||
return
|
||||
}
|
||||
|
||||
window.clearTimeout(req.timer)
|
||||
this.inFlight = null
|
||||
req.resolve(response)
|
||||
this.pump()
|
||||
}
|
||||
}
|
||||
|
||||
var WS = new WSClient()
|
||||
|
||||
class Server {
|
||||
cmd: string
|
||||
|
||||
constructor(cmd: string) {
|
||||
this.cmd = cmd
|
||||
}
|
||||
|
||||
request(data: Object): any {
|
||||
|
||||
var isLogUpdate: boolean = this.cmd == "updateLog"
|
||||
|
||||
// Log polling must never pile up behind a stalled connection.
|
||||
if (isLogUpdate && WS.hasQueued("updateLog")) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isLogUpdate) {
|
||||
showElement("loading", true)
|
||||
UNDO = new Object()
|
||||
setConnectionState("busy")
|
||||
}
|
||||
|
||||
data["cmd"] = this.cmd
|
||||
var requestCmd: string = this.cmd
|
||||
|
||||
WS.send(data).then(function(response) {
|
||||
|
||||
WS_FAILURE_COUNT = 0
|
||||
if (!isLogUpdate) {
|
||||
showElement("loading", false)
|
||||
}
|
||||
setConnectionState("online")
|
||||
|
||||
if (response["status"] == false) {
|
||||
setConnectionState("offline")
|
||||
alert(response["err"])
|
||||
if (response.hasOwnProperty("reload")) {
|
||||
location.reload()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("logoURL")) {
|
||||
var div = (document.getElementById("channel-icon") as HTMLInputElement)
|
||||
div.value = response["logoURL"]
|
||||
div.className = "changed"
|
||||
return
|
||||
}
|
||||
|
||||
switch (requestCmd) {
|
||||
case "updateLog":
|
||||
SERVER["log"] = response["log"]
|
||||
if (document.getElementById("content_log")) {
|
||||
showLogs(false)
|
||||
}
|
||||
return
|
||||
|
||||
default:
|
||||
SERVER = response
|
||||
break
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("openMenu")) {
|
||||
var menu = document.getElementById(response["openMenu"])
|
||||
menu.click()
|
||||
showElement("popup", false)
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("openLink")) {
|
||||
window.location = response["openLink"]
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("alert")) {
|
||||
alert(response["alert"])
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("reload")) {
|
||||
location.reload()
|
||||
}
|
||||
|
||||
if (response.hasOwnProperty("wizard")) {
|
||||
createLayout()
|
||||
configurationWizard[response["wizard"]].createWizard()
|
||||
return
|
||||
}
|
||||
|
||||
createLayout()
|
||||
|
||||
}, function(err) {
|
||||
|
||||
WS_FAILURE_COUNT++
|
||||
if (!isLogUpdate) {
|
||||
showElement("loading", false)
|
||||
}
|
||||
|
||||
var state: string = "offline"
|
||||
if (isLogUpdate && WS_FAILURE_COUNT < 2) {
|
||||
state = "idle"
|
||||
}
|
||||
setConnectionState(state)
|
||||
|
||||
if (WS_AVAILABLE == false && !isLogUpdate && requestCmd != "getServerConfig") {
|
||||
alert("No websocket connection to xTeVe could be established. Check your network configuration.")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var WS_FAILURE_COUNT: number = 0
|
||||
Reference in New Issue
Block a user