Compare commits

..

3 Commits

Author SHA1 Message Date
dd7e7e59e6 feat: move call sound to server 2026-03-07 17:04:55 +01:00
952373bbee feat: remove iprooms, add rooms.txt 2026-03-07 17:01:38 +01:00
c506108ca8 feat: motd 2026-03-07 16:50:56 +01:00
6 changed files with 114 additions and 72 deletions

View File

@@ -1,16 +1,10 @@
import type { ServerWebSocket } from "bun"; import type { ServerWebSocket } from "bun";
import { callTicket, completeTicket, getCurrentTicket, getDisplayTickets, getLogEntries, getTicket, noShowTicket } from "./redis"; import { callTicket, completeTicket, getCurrentTicket, getDisplayTickets, getLogEntries, getTicket, noShowTicket } from "./redis";
let IP_TO_ROOMS: Record<string, string> = {}; const roomstxt = await Bun.file("./rooms.txt").text();
const ROOMS = roomstxt.split("\n");
const iprooms = await Bun.file("./iprooms.csv").text(); const MOTD = process.env.MOTD || "50;";
const lines = iprooms.split("\n");
for (const line of lines.slice(1)) {
const [ip, room] = line.split(",");
if (ip && room) {
IP_TO_ROOMS[ip.trim()] = room.trim();
}
}
export let displaySockets: ServerWebSocket<unknown>[] = []; export let displaySockets: ServerWebSocket<unknown>[] = [];
@@ -34,22 +28,7 @@ Bun.serve({
async message(ws, message) { async message(ws, message) {
console.log("Received message:", message); console.log("Received message:", message);
const data = JSON.parse(message.toString()); const data = JSON.parse(message.toString());
if (data.type === "hello") {} if (data.type === "hello") {
else if (data.type === "my-room") {
console.log("Client requested room for IP:", ws.remoteAddress);
const room = IP_TO_ROOMS[ws.remoteAddress];
if (!room) {
return void ws.send(JSON.stringify({ type: "error", code: -1, message: "No room" }));
}
ws.send(JSON.stringify({ type: "my-room", room }) );
// } else if (data.type === "create-ticket") {
// const ticket = await getTicket(data.ticket);
// if(!ticket || ticket.status == "completed" || ticket.status == "no-show") {
// await createTicket(data.ticket);
// ws.send(JSON.stringify({ type: "create-ticket", status: "created", ticket, num: data.ticket }) );
// return;
// }
// ws.send(JSON.stringify({ type: "error", code: -2, message: "Ticket already exists and is not completed or no-show", ticket, num: data.ticket }));
} else if (data.type === "call-ticket") { } else if (data.type === "call-ticket") {
// const ticket = await getTicket(data.ticket); // const ticket = await getTicket(data.ticket);
// if(!ticket || ticket.status == "completed" || ticket.status == "no-show") { // if(!ticket || ticket.status == "completed" || ticket.status == "no-show") {
@@ -111,8 +90,16 @@ Bun.serve({
if(!displaySockets.includes(ws)) { if(!displaySockets.includes(ws)) {
displaySockets.push(ws); displaySockets.push(ws);
console.log("Added display socket. Total:", displaySockets.length); console.log("Added display socket. Total:", displaySockets.length);
ws.send(JSON.stringify({ type: "display", tickets: await getDisplayTickets() })); ws.send(JSON.stringify({ type: "display", tickets: await getDisplayTickets(), motd: MOTD }));
} }
} else if (data.type == "rooms") {
ws.send(JSON.stringify({ type: "rooms", rooms: ROOMS }));
} else if (data.type == "call-audio") {
// Send the call.wav file as data URI
const file = await Bun.file("./call.wav").arrayBuffer();
const base64 = Buffer.from(file).toString("base64");
const dataUri = `data:audio/wav;base64,${base64}`;
ws.send(JSON.stringify({ type: "call-audio", dataUri }));
} }
}, // a message is received }, // a message is received
open(ws) { open(ws) {

2
rooms.txt Normal file
View File

@@ -0,0 +1,2 @@
Test
Test 2

View File

@@ -2,8 +2,9 @@
import SearchForm from "./search-form.svelte"; import SearchForm from "./search-form.svelte";
import VersionSwitcher from "./switcher.svelte"; import VersionSwitcher from "./switcher.svelte";
import * as Sidebar from "$lib/components/ui/sidebar/index.js"; import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import type { ComponentProps } from "svelte"; import { onMount, type ComponentProps } from "svelte";
import { page } from "$app/state"; import { page } from "$app/state";
import { eventTarget } from "./ws.svelte";
let { ref = $bindable(null), ...restProps }: ComponentProps<typeof Sidebar.Root> = $props(); let { ref = $bindable(null), ...restProps }: ComponentProps<typeof Sidebar.Root> = $props();
type NavData = { type NavData = {
@@ -36,6 +37,10 @@
url: "#", url: "#",
items: [ items: [
{ {
title: "Laden...",
url: "#"
},
/*{
title: "1", title: "1",
url: "/room/1", url: "/room/1",
}, },
@@ -74,7 +79,7 @@
{ {
title: "Empfang 2", title: "Empfang 2",
url: "/room/Empfang%202", url: "/room/Empfang%202",
}, },*/
], ],
}, },
// { // {
@@ -103,10 +108,10 @@
title: "Logs", title: "Logs",
url: "/log", url: "/log",
}, },
{ /*{
title: "Admin", title: "Admin",
url: "/admin", url: "/admin",
}, },*/
] ]
} }
], ],
@@ -136,17 +141,37 @@
title: "Logs", title: "Logs",
url: "/log", url: "/log",
}, },
{ /*{
title: "Admin", title: "Admin",
url: "/", url: "/",
isActive: true isActive: true
}, },*/
] ]
} }
] ]
}; };
const data = $derived(isAdmin ? adminData : regularData); const data = $derived(isAdmin ? adminData : regularData);
onMount(() => {
eventTarget.addEventListener("rooms", (e) => {
if(e instanceof CustomEvent) {
const rooms = e.detail.rooms;
data.navMain[0].items = rooms.map((room: string) => ({
title: room,
url: `/room/${encodeURIComponent(room)}`
}));
}
});
eventTarget.addEventListener("hello", (e) => {
eventTarget.dispatchEvent(new CustomEvent("send", {
detail: {
type: "rooms"
}
}))
})
})
</script> </script>
<Sidebar.Root {...restProps} bind:ref variant="floating"> <Sidebar.Root {...restProps} bind:ref variant="floating">
<Sidebar.Header> <Sidebar.Header>

View File

@@ -1,43 +1,14 @@
<script lang="ts"> <script>
import { goto } from "$app/navigation"; import { sidebarState } from "$lib/sidebar.svelte";
import { eventTarget } from "$lib/ws.svelte"; import { onDestroy, onMount } from "svelte";
import { onMount } from "svelte";
let noRoom = $state(false);
onMount(() => { onMount(() => {
// ws.addEventListener("message", (e) => { sidebarState.open = true;
// const msg = JSON.parse(e.data); })
// if(msg.type === "my-room") {
// goto(`/room/${msg.room}`); onDestroy(() => {
// } sidebarState.open = false;
// });
// ws.addEventListener("open", () => {
// ws.send(JSON.stringify({ type: "my-room" }));
// });
eventTarget.addEventListener("my-room", (e) => {
console.log("Received my-room event", e);
const ev = e as CustomEvent;
// if(ev.detail.room == "entry") {
// goto(`/entry`);
// return;
// }
goto(`/room/${ev.detail.room}`);
});
eventTarget.addEventListener("error", (e) => {
const ev = e as CustomEvent;
if(ev.detail.code === -1) {
noRoom = true;
}
});
eventTarget.addEventListener("hello", (e) => {
eventTarget.dispatchEvent(new CustomEvent("send", { detail: { type: "my-room" } }));
});
}) })
</script> </script>
<h1>Willkommen zu NextUp!</h1> <h1>Willkommen zu NextUp!</h1>
<span>Sie werden zu Ihrem Raum weitergeleitet.</span>
{#if noRoom}
<span class="text-red-500">Kein Raum verfügbar</span>
{/if}

View File

@@ -13,6 +13,7 @@
} }
let withSound = false; let withSound = false;
let callAudioURI = "";
let persisted: PersistedState<CallEntry[]> = new PersistedState("calls", []); let persisted: PersistedState<CallEntry[]> = new PersistedState("calls", []);
let calls = $state<CallEntry[]>($state.snapshot(persisted.current)); let calls = $state<CallEntry[]>($state.snapshot(persisted.current));
let newCalls = $state<CallEntry[]>([]); let newCalls = $state<CallEntry[]>([]);
@@ -24,6 +25,12 @@
withSound = location.search.includes("sound=true"); withSound = location.search.includes("sound=true");
eventTarget.addEventListener("display", (e) => { eventTarget.addEventListener("display", (e) => {
const detail = (e as CustomEvent).detail; const detail = (e as CustomEvent).detail;
if(detail.motd) {
const [speed, ...text] = detail.motd.split(" ");
marqueeText = text.join(" ");
marqueeSpeed = parseInt(speed);
updateMarqueeSpeed();
}
const _newCalls = detail.tickets.filter( const _newCalls = detail.tickets.filter(
(call: CallEntry) => !calls.find((c) => c.num === call.num) (call: CallEntry) => !calls.find((c) => c.num === call.num)
); );
@@ -31,8 +38,8 @@
persisted.current = calls; persisted.current = calls;
connected = true; connected = true;
if (!firstLoad) { if (!firstLoad) {
if (_newCalls.length > 0 && withSound) { if (_newCalls.length > 0 && withSound && callAudioURI) {
const audio = new Audio("/call.wav"); const audio = new Audio(callAudioURI);
audio.play(); audio.play();
} }
newCalls.push(..._newCalls); newCalls.push(..._newCalls);
@@ -46,11 +53,33 @@
eventTarget.dispatchEvent( eventTarget.dispatchEvent(
new CustomEvent("send", { detail: { type: "display" } }) new CustomEvent("send", { detail: { type: "display" } })
); );
if(withSound) {
eventTarget.addEventListener("call-audio", (e) => {
const detail = (e as CustomEvent).detail;
callAudioURI = detail.dataUri;
});
eventTarget.dispatchEvent(
new CustomEvent("send", { detail: { type: "call-audio" } })
);
}
}); });
function updateMarqueeSpeed() {
requestAnimationFrame(() => {
const marqueeContent = document.querySelectorAll(".marquee-content") as NodeListOf<HTMLElement>;
const speed = marqueeSpeed; // px per second
const width = marqueeContent[0].offsetWidth;
marqueeContent[0].style.animationDuration = `${width / speed}s`;
marqueeContent[1].style.animationDuration = `${width / speed}s`;
});
}
const ENTRIES: Record<string, number> = { A: 1, B: 2, C: 1, D: 2, E: 1 }; const ENTRIES: Record<string, number> = { A: 1, B: 2, C: 1, D: 2, E: 1 };
const ENTRIES_PER_TABLE = 10; const ENTRIES_PER_TABLE = 10;
let marqueeSpeed = 50;
let marqueeText = $state("");
</script> </script>
{#if newCalls.length > 0} {#if newCalls.length > 0}
@@ -84,6 +113,10 @@
</div> </div>
{/if} {/if}
<div class="marquee">
<div class="marquee-content">{marqueeText}</div>
<div class="marquee-content">{marqueeText}</div>
</div>
<div class="p-4 flex justify-around gap-4 text-5xl"> <div class="p-4 flex justify-around gap-4 text-5xl">
<!-- <h1 class="text-6xl font-bold">Aufruf</h1> --> <!-- <h1 class="text-6xl font-bold">Aufruf</h1> -->
<table class="self-start"> <table class="self-start">
@@ -136,7 +169,7 @@
</table> </table>
<div <div
class="fixed h-full w-1 top-0 left-1/2 -translate-x-1/2 {wsState.closed class="fixed h-full w-1 top-15 left-1/2 -translate-x-1/2 {wsState.closed
? 'bg-red-800' ? 'bg-red-800'
: !wsState.connected : !wsState.connected
? 'bg-amber-800' ? 'bg-amber-800'
@@ -239,4 +272,28 @@
background-color: var(--accent); background-color: var(--accent);
border: 2px solid var(--background); border: 2px solid var(--background);
} }
.marquee {
display: flex;
overflow: hidden;
width: 100vw;
white-space: nowrap;
font-size: 2.5rem;
gap: 4rem;
}
.marquee-content {
animation-iteration-count: infinite;
animation-name: marquee;
animation-timing-function: linear;
}
@keyframes marquee {
from {
transform: translateX(0%);
}
to {
transform: translateX(-100%);
}
}
</style> </style>