Files
swyx/examples/browser-client.html
T
Sven 78b17a1350 SwyxTray und Firefox-Plugin in das Repository aufnehmen
Windows-Tray-Anwendung (CLMgr-Anbindung an SwyxIt!) samt WebSocket-Zugang,
Firefox-Erweiterung und Browser-Beispielclient.
2026-08-21 11:33:52 +02:00

185 lines
6.4 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>SwyxTray — WebSocket-Beispiel</title>
<style>
body { font: 15px/1.5 system-ui, sans-serif; max-width: 46rem; margin: 2rem auto; padding: 0 1rem; }
fieldset { border: 1px solid #ccc; border-radius: 6px; margin-bottom: 1rem; }
input, button { font: inherit; padding: .35rem .6rem; }
input[type=text] { width: 22rem; }
#zustand { font-weight: 600; }
#protokoll { background: #f6f6f6; padding: .75rem; height: 16rem; overflow: auto;
white-space: pre-wrap; font: 13px/1.45 ui-monospace, monospace; }
</style>
</head>
<body>
<h1>SwyxTray — WebSocket-Beispiel</h1>
<p>
<strong>Zur Zeit sind alle Prüfungen abgeschaltet</strong> — es genügt
<code>ws://127.0.0.1:17654/</code> ohne Token, auch beim Öffnen dieser Seite
direkt aus dem Dateisystem.
</p>
<p>
Von einem <strong>anderen Rechner</strong> aus: in
<code>websocket.json</code> <code>"AllowRemoteAccess": true</code> setzen, App
neu starten und die LAN-Adresse verwenden
(<code>ws://192.168.x.y:17654/…</code>). Diese Seite muss dann über
<code>http://</code> ausgeliefert werden — aus einer HTTPS-Seite heraus
blockiert der Browser <code>ws://</code> auf eine entfernte Adresse als Mixed
Content. Für Loopback gilt diese Ausnahme, für die LAN-Adresse nicht.
</p>
<p>
Sind die Prüfungen wieder eingeschaltet, muss die Seite von einem Origin
geladen werden, der in <code>%LOCALAPPDATA%\SwyxTray\websocket.json</code>
unter <code>AllowedOrigins</code> steht, und die Adresse braucht das Token.
Über <code>file://</code> sendet der Browser den Origin <code>null</code>
dann muss dort <code>"null"</code> eingetragen sein.
</p>
<fieldset>
<legend>Verbindung</legend>
<input type="text" id="url" placeholder="ws://127.0.0.1:17654/?token=…">
<button id="verbinden">Verbinden</button>
<p>Status: <span id="zustand">nicht verbunden</span></p>
</fieldset>
<fieldset>
<legend>Steuerung</legend>
<input type="text" id="nummer" placeholder="+49 30 1234567">
<button data-cmd="call">Anrufen</button>
<button data-cmd="answer">Annehmen</button>
<button data-cmd="hangup">Auflegen</button>
<button data-cmd="hold">Halten</button>
<button data-cmd="status">Status</button>
<p>
<input type="text" id="fenstertitel" placeholder="Tab-Titel (optional)">
<input type="text" id="fokusurl" placeholder="https://… (Pflicht)">
<button data-cmd="focus">Fokussieren</button>
</p>
</fieldset>
<fieldset>
<legend>Tabs (Firefox-Plugin)</legend>
<p>
<button data-cmd="tabs">Tabs anzeigen</button>
<input type="text" id="taburl" placeholder="https://…">
<button data-cmd="opentab">Tab öffnen</button>
</p>
<ul id="tabliste"></ul>
</fieldset>
<div id="protokoll"></div>
<script>
let ws = null;
let naechsteId = 1;
let absichtlichGetrennt = false;
const gesendet = new Map(); // id → cmd, um Antworten dem Kommando zuzuordnen
const $ = id => document.getElementById(id);
const log = text => {
$("protokoll").textContent += text + "\n";
$("protokoll").scrollTop = $("protokoll").scrollHeight;
};
function verbinden() {
absichtlichGetrennt = false;
ws = new WebSocket($("url").value.trim(), ["swyxtray.v1"]);
ws.onopen = () => { $("zustand").textContent = "verbunden"; log("→ verbunden"); };
ws.onmessage = e => {
const m = JSON.parse(e.data);
log("← " + e.data);
// Der Zustand kommt unaufgefordert bei jeder Änderung — die Seite muss
// ihn also nie selbst nachhalten.
if (m.type === "snapshot") {
const belegt = m.lines.filter(l => l.busy);
$("zustand").textContent = m.statusText +
(belegt.length ? " — " + belegt.map(l => `L${l.line}: ${l.stateText} ${l.peer}`).join(", ") : "");
}
// Anruf-Ereignisse kommen zusätzlich zum snapshot — hier reagiert eine
// echte Anwendung, z. B. mit dem Öffnen der Kundenakte zum Anrufer.
if (m.type === "call") {
const text = { incoming: "Eingehender Ruf", outgoing: "Ausgehender Ruf",
connected: "Gespräch verbunden", ended: "Gespräch beendet" }[m.event] ?? m.event;
log(`☎ ${text} — Leitung ${m.line}: ${m.peer}`);
}
if (m.type === "result") {
const cmd = gesendet.get(m.id);
gesendet.delete(m.id);
if (cmd === "tabs" && m.ok) zeigeTabs(m.tabs ?? []);
// Nach Öffnen oder Schließen die Liste auffrischen, sofern sie schon
// einmal angezeigt wurde — der Zustand kommt hier nicht von selbst.
if ((cmd === "opentab" || cmd === "closetab") && m.ok
&& $("tabliste").childElementCount > 0) senden("tabs");
}
};
ws.onerror = () => log("→ Fehler (Token? Origin? Läuft SwyxTray?)");
// WebSockets verbinden sich nicht von selbst neu — anders als EventSource.
ws.onclose = e => {
$("zustand").textContent = "getrennt";
log(`→ getrennt (Code ${e.code})`);
if (!absichtlichGetrennt) setTimeout(verbinden, 2000);
};
}
function senden(cmd, felder = {}) {
if (ws?.readyState !== WebSocket.OPEN) { log("→ nicht verbunden"); return; }
const nachricht = { id: naechsteId++, cmd, ...felder };
if (cmd === "call") nachricht.number = $("nummer").value.trim();
if (cmd === "focus") {
nachricht.title = $("fenstertitel").value.trim();
const url = $("fokusurl").value.trim();
if (url) nachricht.url = url;
}
if (cmd === "opentab") {
const url = $("taburl").value.trim();
if (url) nachricht.url = url;
}
gesendet.set(nachricht.id, cmd);
log("→ " + JSON.stringify(nachricht));
ws.send(JSON.stringify(nachricht));
}
// Baut die Liste per createElement auf — Titel und Adressen stammen aus dem
// Browser und gehören nicht ungeprüft in innerHTML.
function zeigeTabs(tabs) {
const liste = $("tabliste");
liste.replaceChildren();
for (const tab of tabs) {
const li = document.createElement("li");
const knopf = document.createElement("button");
knopf.textContent = "Schließen";
knopf.onclick = () => senden("closetab", { tabId: tab.id });
li.append(knopf, ` ${tab.active ? "▶ " : ""}${tab.title ?? ""}${tab.url ?? ""}`);
liste.append(li);
}
if (!tabs.length) {
const li = document.createElement("li");
li.textContent = "(keine Tabs)";
liste.append(li);
}
}
$("verbinden").onclick = () => {
absichtlichGetrennt = true;
ws?.close();
verbinden();
};
document.querySelectorAll("button[data-cmd]").forEach(
b => b.onclick = () => senden(b.dataset.cmd));
</script>
</body>
</html>