chore: release v1.0.7
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:
1 parent
17ac04856b
commit
5b5cdc4cd8
26 files changed
+1723
-110
No files matched your search
@@ -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",
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user