chore: release v1.0.7
CI / test (push) Successful in 9m11s
CI / docker (push) Failing after 2m39s
CI / release (push) Skipped
Release / release (push) Canceled after 2m11s

Playout: slate fallback, strict ffprobe validation, Prometheus metrics
Chat: history on join, DM offline delivery, env-config rate limits,
  Redis auto-reconnect, heartbeat, ban/mute enforcement, DM metrics
Viewer: presence indicators, schedule overlay, public /api/schedule
Admin: playlist drag-reorder, schedule overrides CRUD, user actions,
  inline mod buttons, CSV export, config editor, restart buttons,
  upload normalization gate, analytics time range
nginx: CORS preflight, JSON health, stub_status
Docs: test-lab vs production distinction across all configs/docs
Infra: Redis maxmemory, CD health IP-agnostic, slate volume mount
This commit is contained in:
zwitschi committed 2026-08-06 17:41:52 +02:00
1 parent 17ac04856b
commit 5b5cdc4cd8
26 files changed
+1723 -110

No files matched your search

+49
View File
@@ -16,6 +16,7 @@ let ws: WebSocket | null = null;
let currentRoom = "main";
let username = "viewer" + Math.floor(Math.random() * 1000);
let chatVisible = true;
const onlineUsers = new Set<string>();
function resolveWebSocketBaseUrl() {
const configuredUrl = import.meta.env.VITE_PUBLIC_WS_URL?.trim();
@@ -74,6 +75,19 @@ function connectWebSocket(room: string) {
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
addMessage(msg.type || "public", msg.text, msg.username, msg.timestamp);
// Track presence from system join/leave messages
if (msg.type === "system" && msg.username === "system") {
const joined = msg.text?.match(/^(\S+) joined /);
const left = msg.text?.match(/^(\S+) left /);
if (joined) {
onlineUsers.add(joined[1]);
renderPresence();
} else if (left) {
onlineUsers.delete(left[1]);
renderPresence();
}
}
};
ws.onclose = () => {
@@ -112,6 +126,37 @@ function addMessage(
messages.scrollTop = messages.scrollHeight;
}
function renderPresence() {
const list = document.getElementById("presence-list");
if (!list) return;
const sorted = [...onlineUsers].sort();
list.innerHTML = sorted
.map(
(u) =>
`<span class="presence-dot" title="${u}"><span class="presence-indicator"></span>${u}</span>`,
)
.join("");
}
async function refreshSchedule() {
try {
const res = await fetch("/api/schedule");
if (!res.ok) return;
const data = await res.json();
const entries = data.entries as Array<{ title: string; status: string }>;
if (entries.length > 0) {
const np = document.getElementById("np-title");
const nn = document.getElementById("np-next");
if (np) np.textContent = `Now: ${entries[0].title}`;
if (nn && entries.length > 1) {
nn.textContent = `Up next: ${entries[1].title}`;
}
}
} catch {
// Schedule unavailable — non-critical
}
}
async function initPlayerAndChat() {
const video = document.getElementById("player") as HTMLVideoElement;
const manifestUrl = resolveManifestUrl();
@@ -136,6 +181,10 @@ async function initPlayerAndChat() {
connectWebSocket("main");
// Start schedule polling
refreshSchedule();
setInterval(refreshSchedule, 30000);
console.log(
"Linear Void Frontend initialized - HLS Player + Chat Sidebar ready",
);