Zum Inhalt springen

Web-Frontend entwickeln: Unterschied zwischen den Versionen

Aus MegBes Wiki
Clawtronics (Diskussion | Beiträge)
Ausfuehrliche HTML- und JavaScript-Entwicklerdokumentation aus aktuellem Repository erstellt
 
Clawtronics (Diskussion | Beiträge)
Erkenntnisse aus GKP-Frontendprüfung: Hash-Routing, Sitzungsdaten, Hosting und Reproduzierbarkeit
 
(6 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 3: Zeile 3:
== Zweck und Geltungsbereich ==
== Zweck und Geltungsbereich ==


Diese Seite beschreibt die HTML-/JavaScript-Anbindung eines Web-Frontends an `megbes-external`. Sie basiert auf den Controllern und Request-Modellen des aktuellen MegBes-Repositorys. Vor der Freigabe eines Frontends muss trotzdem geprüft werden, welcher MegBes-Release im betreffenden Projekt installiert ist.
Diese Seite beschreibt die HTML-/JavaScript-Anbindung eines Web-Frontends an <code>megbes-external</code>. Sie basiert auf den Controllern und Request-Modellen des aktuellen MegBes-Repositorys. Vor der Freigabe eines Frontends muss trotzdem geprüft werden, welcher MegBes-Release im betreffenden Projekt installiert ist.


Das Frontend spricht ausschlie&szlig;lich `megbes-external` an. Direkte Browseraufrufe an `megbes-internal` sind nicht vorgesehen.
Das Frontend spricht ausschließlich <code>megbes-external</code> an. Direkte Browseraufrufe an <code>megbes-internal</code> sind nicht vorgesehen.


  Browser &rarr; HTTPS &rarr; megbes-external &rarr; megbes-internal &rarr; UDP &rarr; Station
  Browser HTTPS megbes-external megbes-internal UDP Station


== Vor Beginn festlegen ==
== Vor Beginn festlegen ==
Zeile 16: Zeile 16:
|-
|-
| API-Basisadresse || HTTPS-Adresse der projektspezifischen External-Instanz
| API-Basisadresse || HTTPS-Adresse der projektspezifischen External-Instanz
| Projektkennung || Wert f&uuml;r `prSc`; gegen den eingesetzten Release und die Projektkonfiguration pr&uuml;fen
|-
| Projektkennung || Wert für <code>prSc</code>; gegen den eingesetzten Release und die Projektkonfiguration prüfen
|-
| Sicherheitsmodus || ohne Authentifizierung oder zentrale Ticket-/Benutzerauthentifizierung
| Sicherheitsmodus || ohne Authentifizierung oder zentrale Ticket-/Benutzerauthentifizierung
| Stationssteuerung || extern oder durch `megbes-internal` kontrolliert
|-
| Token-Header || Wert der Laufzeitkonfiguration `jwt.header`; nicht pauschal `Authorization` annehmen
| Stationssteuerung || extern oder durch <code>megbes-internal</code> kontrolliert
|-
| Token-Header || Wert der Laufzeitkonfiguration <code>jwt.header</code>; nicht pauschal <code>Authorization</code> annehmen
|-
| Stationsnummern || freigegebene Stationen des Projekts
| Stationsnummern || freigegebene Stationen des Projekts
| Stationsbefehle || projektspezifische Befehle, die die jeweilige Station tats&auml;chlich versteht
|-
| Stationsbefehle || projektspezifische Befehle, die die jeweilige Station tatsächlich versteht
|}
|}


Domains, Headername, Projektkennung, Stationsnummern und Befehle d&uuml;rfen nicht ungepr&uuml;ft von einem anderen Projekt &uuml;bernommen werden.
Domains, Headername, Projektkennung, Stationsnummern und Befehle dürfen nicht ungeprüft von einem anderen Projekt übernommen werden.


== API-&Uuml;bersicht ==
== Betriebsarten und Stationsstatus ==
 
{| class="wikitable"
! Betriebsart
! Konfiguration
! Verhalten des Frontends
|-
| extern verwaltete Stationsbelegung || <code>megbes.internal.station.controlled=false</code> || Station über <code>GET /media-station?station=...</code> belegen; den Status-Endpunkt nicht aufrufen
|-
| durch <code>megbes-internal</code> kontrollierte Station || <code>megbes.internal.station.controlled=true</code> || nach der Stationsanmeldung zusätzlich <code>GET /media-station/status/{stationNumber}</code> auswerten
|}
 
Liefert der Status-Endpunkt <code>404 Not Found</code>, obwohl Anmeldung und Stationsbelegung funktionieren, ist zuerst die Betriebsart zu prüfen. Bei extern verwalteter Stationsbelegung ist <code>404</code> erwartbar und bedeutet nicht, dass die Station belegt oder gestört ist. Die Betriebsart soll als zentraler Projektwert gepflegt und nicht aus einer fehlgeschlagenen Statusabfrage erraten werden.
== API-Übersicht ==


{| class="wikitable"
{| class="wikitable"
Zeile 34: Zeile 53:
! Erfolgsantwort
! Erfolgsantwort
|-
|-
| `GET /info` || vom Server empfangenen User-Agent pr&uuml;fen || frei || `200` mit User-Agent
| <code>GET /info</code> || vom Server empfangenen User-Agent prüfen || frei || <code>200</code> mit User-Agent
|-
|-
| `POST /authenticate` || Ticket-Monitor administrativ authentifizieren || frei; ben&ouml;tigt Header `customer` und `authId` || `200` und `{ "id": "..." }`
| <code>POST /authenticate</code> || Ticket-Monitor administrativ authentifizieren || frei; benötigt Header <code>customer</code> und <code>authId</code> || <code>200</code> und <code>{ "id": "..." }</code>
|-
|-
| `GET /verify-admin` || Administrationskennung pr&uuml;fen || ADMIN || `200`
| <code>GET /verify-admin</code> || Administrationskennung prüfen || ADMIN || <code>200</code>
|-
|-
| `GET /ticket` || Ticketcode erzeugen oder vorhandenen unbenutzten Code abrufen || ADMIN || `200` und `{ "id": "......" }`
| <code>GET /ticket</code> || Ticketcode erzeugen oder vorhandenen unbenutzten Code abrufen || ADMIN || <code>200</code> und <code>{ "id": "......" }</code>
|-
|-
| `POST /enter-system` || Ticketcode einmalig aktivieren || frei || `200`
| <code>POST /enter-system</code> || Ticketcode einmalig aktivieren || frei || <code>200</code>
|-
|-
| `GET /verify` || aktivierte Benutzerkennung pr&uuml;fen || USER oder ADMIN || `200`
| <code>GET /verify</code> || aktivierte Benutzerkennung prüfen || USER oder ADMIN || <code>200</code>
|-
|-
| `GET /media-station?station=...` || Benutzer an Station anmelden || USER oder ADMIN || `200`
| <code>GET /media-station?station=...</code> || Benutzer an Station anmelden || USER oder ADMIN || <code>200</code>
|-
|-
| `GET /media-station/status/{stationNumber}` || Stationszustand abrufen || USER oder ADMIN; nur bei intern kontrollierten Stationen || `200` und `{ "stationStatus": "..." }`
| <code>GET /media-station/status/{stationNumber}</code> || Stationszustand abrufen || USER oder ADMIN; nur bei intern kontrollierten Stationen || <code>200</code> und <code>{ "stationStatus": "..." }</code>
|-
|-
| `POST /video-command` || einen oder mehrere UDP-Befehle senden || ohne Security frei, sonst USER oder ADMIN || Antwort von `megbes-internal`
| <code>POST /video-command</code> || einen oder mehrere UDP-Befehle senden || ohne Security frei, sonst USER oder ADMIN || <code>200</code> mit leerem Antwortkörper
|-
|-
| `DELETE /media-station` || Benutzer abmelden und Station zur&uuml;cksetzen || USER oder ADMIN || `200`
| <code>DELETE /media-station</code> || Benutzer abmelden und Station zurücksetzen || USER oder ADMIN || <code>200</code>
|}
|}


== Wichtige Authentifizierungsregel ==
== Wichtige Authentifizierungsregel ==


MegBes liefert dem Browser beziehungsweise Ticket-Monitor eine '''Pseudo-ID'''. Der eigentliche JWT verbleibt serverseitig. Bei gesch&uuml;tzten Folgeaufrufen sendet das Frontend die Pseudo-ID als Wert des projektspezifisch konfigurierten Token-Headers.
MegBes liefert dem Browser beziehungsweise Ticket-Monitor eine '''Pseudo-ID'''. Der eigentliche JWT verbleibt serverseitig. Bei geschützten Folgeaufrufen sendet das Frontend die Pseudo-ID als Wert des projektspezifisch konfigurierten Token-Headers.


  <nowiki><konfigurierter-token-header>: <Pseudo-ID></nowiki>
  <nowiki><konfigurierter-token-header>: <Pseudo-ID></nowiki>


Der Headername stammt aus `jwt.header`. Er darf im Frontend nicht geraten werden. Die Pseudo-ID ist wie eine Sitzungskennung zu behandeln: nicht protokollieren, nicht in URLs anh&auml;ngen und nicht ins Wiki kopieren.
Der Headername stammt aus <code>jwt.header</code>. Er darf im Frontend nicht geraten werden. Die Pseudo-ID ist wie eine Sitzungskennung zu behandeln: nicht protokollieren, nicht in URLs anhängen und nicht ins Wiki kopieren.


== Request-Modelle ==
== Request-Modelle ==
Zeile 67: Zeile 86:
=== System betreten ===
=== System betreten ===


`POST /enter-system`
<code>POST /enter-system</code>


<syntaxhighlight lang="json">
<pre>
{
{
   "systemCode": "123456"
   "systemCode": "123456"
}
}
</syntaxhighlight>
</pre>


`systemCode` darf nicht leer sein. Ein unbekannter oder bereits verwendeter Code ergibt `401 Unauthorized`.
<code>systemCode</code> darf nicht leer sein. Ein unbekannter oder bereits verwendeter Code ergibt <code>401 Unauthorized</code>.


=== Stationsbefehl ===
=== Stationsbefehl ===


`POST /video-command`
<code>POST /video-command</code>


<syntaxhighlight lang="json">
<pre>
{
{
   "prSc": "<Projektkennung>",
   "prSc": "<Projektkennung>",
Zeile 88: Zeile 107:
   ]
   ]
}
}
</syntaxhighlight>
</pre>


Regeln des aktuellen Request-Modells:
Regeln des aktuellen Request-Modells:


* `prSc` ist Pflicht und darf nicht leer sein.
* <code>prSc</code> ist Pflicht und darf nicht leer sein.
* `statNoCommands` ist eine nicht leere Liste.
* <code>statNoCommands</code> ist eine nicht leere Liste.
* Jeder Listeneintrag folgt `\d{0,3}-[0-9a-zA-Z]{2,20}`.
* Jeder Listeneintrag folgt <code>\d{0,3}-[0-9a-zA-Z]{2,20}</code>.
* Vor dem Bindestrich sind damit null bis drei Ziffern zul&auml;ssig; f&uuml;r produktive Stationsbefehle soll eine eindeutige, im Projekt dokumentierte Stationsnummer verwendet werden.
* Vor dem Bindestrich sind damit null bis drei Ziffern zulässig; für produktive Stationsbefehle soll eine eindeutige, im Projekt dokumentierte Stationsnummer verwendet werden.
* Nach dem Bindestrich sind zwei bis zwanzig ASCII-Buchstaben oder Ziffern zul&auml;ssig.
* Nach dem Bindestrich sind zwei bis zwanzig ASCII-Buchstaben oder Ziffern zulässig.
* Ob ein formal g&uuml;ltiger Befehl fachlich unterst&uuml;tzt wird, entscheidet die jeweilige Stationssoftware.
* Ob ein formal gültiger Befehl fachlich unterstützt wird, entscheidet die jeweilige Stationssoftware.


Beispiele wie `12-play` sind nur Formbeispiele und keine globale MegBes-Befehlsliste.
Beispiele wie <code>12-play</code> sind nur Formbeispiele und keine globale MegBes-Befehlsliste.


== Empfohlener JavaScript-API-Client ==
== Empfohlener JavaScript-API-Client ==
Zeile 105: Zeile 124:
Die Netzwerkanbindung soll an einer Stelle gekapselt werden. Dadurch bleiben Fehlerbehandlung, Header und JSON-Verarbeitung konsistent.
Die Netzwerkanbindung soll an einer Stelle gekapselt werden. Dadurch bleiben Fehlerbehandlung, Header und JSON-Verarbeitung konsistent.


<syntaxhighlight lang="javascript">
<pre>
const megBesConfig = Object.freeze({
const megBesConfig = Object.freeze({
   apiBase: "https://<projekt>.megbes.de",
   apiBase: "https://<projekt>.megbes.de",
Zeile 163: Zeile 182:
   return responseBody;
   return responseBody;
}
}
</syntaxhighlight>
</pre>


== Variante A: Frontend ohne Authentifizierung ==
== Variante A: Frontend ohne Authentifizierung ==


Diese Variante existiert nur, wenn `megbes.security` f&uuml;r die External-Instanz nicht aktiviert ist. Das Frontend darf diesen Modus nicht anhand eines fehlgeschlagenen Loginversuchs erraten; er wird vor der Entwicklung projektspezifisch festgelegt.
Diese Variante existiert nur, wenn <code>megbes.security</code> für die External-Instanz nicht aktiviert ist. Das Frontend darf diesen Modus nicht anhand eines fehlgeschlagenen Loginversuchs erraten; er wird vor der Entwicklung projektspezifisch festgelegt.


<syntaxhighlight lang="javascript">
<pre>
async function sendCommandWithoutAuthentication(station, command) {
async function sendCommandWithoutAuthentication(station, command) {
   return megBesRequest("/video-command", {
   return megBesRequest("/video-command", {
Zeile 179: Zeile 198:
   });
   });
}
}
</syntaxhighlight>
</pre>


Auch ohne MegBes-Authentifizierung muss die External-Instanz ausschlie&szlig;lich &uuml;ber HTTPS angesprochen werden. Welche Personen die Seite erreichen d&uuml;rfen, ist au&szlig;erhalb von MegBes abzusichern.
Auch ohne MegBes-Authentifizierung muss die External-Instanz ausschließlich über HTTPS angesprochen werden. Welche Personen die Seite erreichen dürfen, ist außerhalb von MegBes abzusichern.


== Variante B: Ticket-Monitor ==
== Variante B: Ticket-Monitor ==
Zeile 187: Zeile 206:
=== 1. Ticket-Monitor authentifizieren ===
=== 1. Ticket-Monitor authentifizieren ===


Der Ticket-Monitor ruft `POST /authenticate` mit den Headern `customer` und `authId` auf. Zus&auml;tzlich pr&uuml;ft MegBes den User-Agent gegen die Projektkonfiguration.
Der Ticket-Monitor ruft <code>POST /authenticate</code> mit den Headern <code>customer</code> und <code>authId</code> auf. Zusätzlich prüft MegBes den User-Agent gegen die Projektkonfiguration.


<syntaxhighlight lang="javascript">
<pre>
async function authenticateTicketMonitor(customer, authId) {
async function authenticateTicketMonitor(customer, authId) {
   const response = await megBesRequest("/authenticate", {
   const response = await megBesRequest("/authenticate", {
Zeile 200: Zeile 219:
   return response.id;
   return response.id;
}
}
</syntaxhighlight>
</pre>


`authId` ist ein Geheimwert und darf nicht fest in ausgelieferter Browser-JavaScript-Datei stehen. Wenn der Ticket-Monitor nur statische &ouml;ffentliche Dateien ausliefert, ist dieses Verfahren sicherheitlich neu zu bewerten. Die Kennung geh&ouml;rt in eine gesch&uuml;tzte Laufzeitumgebung oder einen vertrauensw&uuml;rdigen lokalen Dienst.
<code>authId</code> ist ein Geheimwert und darf nicht fest in ausgelieferter Browser-JavaScript-Datei stehen. Wenn der Ticket-Monitor nur statische öffentliche Dateien ausliefert, ist dieses Verfahren sicherheitlich neu zu bewerten. Die Kennung gehört in eine geschützte Laufzeitumgebung oder einen vertrauenswürdigen lokalen Dienst.


=== 2. Ticketcode abrufen ===
=== 2. Ticketcode abrufen ===


<syntaxhighlight lang="javascript">
<pre>
async function getTicket(adminToken, customer, ticketMonitorNumber) {
async function getTicket(adminToken, customer, ticketMonitorNumber) {
   const query = new URLSearchParams({
   const query = new URLSearchParams({
Zeile 218: Zeile 237:
   return response.id;
   return response.id;
}
}
</syntaxhighlight>
</pre>


Der gelieferte Code ist im aktuellen Stand sechsstellig. Das Frontend soll ihn als String behandeln, damit eine f&uuml;hrende Null nicht verloren geht.
Der gelieferte Code ist im aktuellen Stand sechsstellig. Das Frontend soll ihn als String behandeln, damit eine führende Null nicht verloren geht.


== Variante C: Besucher- oder Guide-Frontend mit Ticketcode ==
== Variante C: Besucher- oder Guide-Frontend mit Ticketcode ==
Zeile 226: Zeile 245:
=== 1. Code aktivieren ===
=== 1. Code aktivieren ===


<syntaxhighlight lang="javascript">
<pre>
async function enterSystem(systemCode) {
async function enterSystem(systemCode) {
   await megBesRequest("/enter-system", {
   await megBesRequest("/enter-system", {
Zeile 234: Zeile 253:
   return systemCode;
   return systemCode;
}
}
</syntaxhighlight>
</pre>


Nach erfolgreichem Eintritt dient derselbe Code als Pseudo-ID f&uuml;r die weiteren Aufrufe. Ein Ticketcode kann nur einmal aktiviert werden.
Nach erfolgreichem Eintritt dient derselbe Code als Pseudo-ID für die weiteren Aufrufe. Ein Ticketcode kann nur einmal aktiviert werden.


=== 2. Kennung pr&uuml;fen ===
=== 2. Kennung prüfen ===


<syntaxhighlight lang="javascript">
<pre>
async function verifyUser(userToken) {
async function verifyUser(userToken) {
   await megBesRequest("/verify", { token: userToken });
   await megBesRequest("/verify", { token: userToken });
   return true;
   return true;
}
}
</syntaxhighlight>
</pre>


=== 3. An Station anmelden ===
=== 3. An Station anmelden ===


<syntaxhighlight lang="javascript">
<pre>
async function loginStation(userToken, stationNumber) {
async function loginStation(userToken, stationNumber) {
   const query = new URLSearchParams({ station: String(stationNumber) });
   const query = new URLSearchParams({ station: String(stationNumber) });
   await megBesRequest(`/media-station?${query}`, { token: userToken });
   await megBesRequest(`/media-station?${query}`, { token: userToken });
}
}
</syntaxhighlight>
</pre>


Im intern kontrollierten Modus ist die Stationsnummer zwingend. Ein `401` kann neben einer ung&uuml;ltigen Kennung auch bedeuten, dass die Station belegt ist oder die Stationsanmeldung abgelehnt wurde. Die Bedienoberfl&auml;che darf deshalb nicht automatisch behaupten, der Ticketcode sei falsch.
Im intern kontrollierten Modus ist die Stationsnummer zwingend. Ein <code>401</code> kann neben einer ungültigen Kennung auch bedeuten, dass die Station belegt ist oder die Stationsanmeldung abgelehnt wurde. Die Bedienoberfläche darf deshalb nicht automatisch behaupten, der Ticketcode sei falsch.


=== 4. Optional Stationsstatus pr&uuml;fen ===
=== 4. Optional Stationsstatus prüfen ===


Dieser Endpunkt ist nur verf&uuml;gbar, wenn `megbes.internal.station.controlled=true` gilt.
Dieser Endpunkt ist nur verfügbar, wenn <code>megbes.internal.station.controlled=true</code> gilt.


<syntaxhighlight lang="javascript">
<pre>
async function getStationStatus(userToken, stationNumber) {
async function getStationStatus(userToken, stationNumber) {
   const response = await megBesRequest(
   const response = await megBesRequest(
Zeile 270: Zeile 289:
   return response.stationStatus;
   return response.stationStatus;
}
}
</syntaxhighlight>
</pre>


Der aktuelle Code verwendet unter anderem die Zust&auml;nde `OPEN`, `IDLE`, `REMOTE` und `TOUCHSCREEN`. Die Oberfl&auml;che soll unbekannte k&uuml;nftige Werte neutral anzeigen und nicht abst&uuml;rzen.
Der aktuelle Code verwendet unter anderem die Zustände <code>OPEN</code>, <code>IDLE</code>, <code>REMOTE</code> und <code>TOUCHSCREEN</code>. Die Oberfläche soll unbekannte künftige Werte neutral anzeigen und nicht abstürzen.


=== 5. Befehl senden ===
=== 5. Befehl senden ===


<syntaxhighlight lang="javascript">
<pre>
async function sendCommand(userToken, stationNumber, command) {
async function sendCommand(userToken, stationNumber, command) {
   return megBesRequest("/video-command", {
   return megBesRequest("/video-command", {
Zeile 287: Zeile 306:
   });
   });
}
}
</syntaxhighlight>
</pre>
 
Der erfolgreiche Aufruf antwortet derzeit mit <code>200 OK</code> und leerem Body. JavaScript-Clients dürfen deshalb nicht zwingend JSON parsen. Bei jQuery ist insbesondere <code>dataType: 'json'</code> für diesen Aufruf ungeeignet. Eine Navigation oder Erfolgsmeldung darf nur im Erfolgszweig (<code>.done()</code> beziehungsweise <code>response.ok</code>) erfolgen, nicht in <code>.always()</code> oder <code>finally</code>.


=== 6. Station abmelden ===
=== 6. Station abmelden ===


<syntaxhighlight lang="javascript">
<pre>
async function logoutStation(userToken) {
async function logoutStation(userToken) {
   await megBesRequest("/media-station", {
   await megBesRequest("/media-station", {
Zeile 298: Zeile 319:
   });
   });
}
}
</syntaxhighlight>
</pre>


Die Abmeldung soll &uuml;ber eine sichtbare Bedienhandlung und bei regul&auml;rem Abschluss erfolgen. Ein Aufruf in `beforeunload` ist allein nicht zuverl&auml;ssig, da Browser ausstehende Requests beim Schlie&szlig;en abbrechen k&ouml;nnen. Der projektspezifische Stations-Timer bleibt daher als R&uuml;ckfallebene erforderlich.
Die Abmeldung soll über eine sichtbare Bedienhandlung und bei regulärem Abschluss erfolgen. Ein Aufruf in <code>beforeunload</code> ist allein nicht zuverlässig, da Browser ausstehende Requests beim Schließen abbrechen können. Der projektspezifische Stations-Timer bleibt daher als Rückfallebene erforderlich.


== Vollst&auml;ndiger Ablauf eines Besucher-Frontends ==
<code>DELETE /media-station</code> gibt nur die aktuell belegte Station frei und setzt deren Stationszustand zurück. Die Ticket- beziehungsweise Benutzersitzung bleibt gültig; ein anschließendes <code>GET /verify</code> kann daher weiterhin <code>200 OK</code> liefern. So kann derselbe Besucher danach eine andere Station auswählen.


<syntaxhighlight lang="javascript">
== Vollständiger Ablauf eines Besucher-Frontends ==
 
<pre>
let userToken = null;
let userToken = null;
let activeStation = null;
let activeStation = null;
Zeile 333: Zeile 356:
   activeStation = null;
   activeStation = null;
}
}
</syntaxhighlight>
</pre>


== Minimales HTML-Beispiel ==
== Minimales HTML-Beispiel ==
Zeile 339: Zeile 362:
Das Beispiel zeigt die Struktur, ist aber ohne projektspezifische Werte und Gestaltung nicht produktionsfertig.
Das Beispiel zeigt die Struktur, ist aber ohne projektspezifische Werte und Gestaltung nicht produktionsfertig.


<syntaxhighlight lang="html">
<pre>
<!doctype html>
<!doctype html>
<html lang="de">
<html lang="de">
Zeile 413: Zeile 436:
</body>
</body>
</html>
</html>
</syntaxhighlight>
</pre>


Die sichtbaren Befehle `play` und `stop` sind ebenfalls nur Platzhalter. Sie werden durch die freigegebenen Projektbefehle ersetzt.
Die sichtbaren Befehle <code>play</code> und <code>stop</code> sind ebenfalls nur Platzhalter. Sie werden durch die freigegebenen Projektbefehle ersetzt.


== Fehlerbehandlung ==
== Fehlerbehandlung ==


<syntaxhighlight lang="javascript">
<pre>
function messageForMegBesError(error) {
function messageForMegBesError(error) {
   if (!(error instanceof MegBesError)) {
   if (!(error instanceof MegBesError)) {
Zeile 429: Zeile 452:
       return "MegBes ist derzeit nicht erreichbar.";
       return "MegBes ist derzeit nicht erreichbar.";
     case 400:
     case 400:
       return "Die Anfrage ist unvollst&auml;ndig oder ung&uuml;ltig.";
       return "Die Anfrage ist unvollständig oder ungültig.";
     case 401:
     case 401:
       return "Anmeldung, Berechtigung oder Stationsbelegung konnte nicht best&auml;tigt werden.";
       return "Anmeldung, Berechtigung oder Stationsbelegung konnte nicht bestätigt werden.";
     case 403:
     case 403:
       return "Die Station ist f&uuml;r diese Sitzung nicht freigegeben.";
       return "Die Station ist für diese Sitzung nicht freigegeben.";
     case 404:
     case 404:
       return "Diese Funktion ist in der aktuellen Betriebsart nicht verf&uuml;gbar.";
       return "Diese Funktion ist in der aktuellen Betriebsart nicht verfügbar.";
     default:
     default:
       return "MegBes konnte den Auftrag nicht ausf&uuml;hren.";
       return "MegBes konnte den Auftrag nicht ausführen.";
   }
   }
}
}
</syntaxhighlight>
</pre>


Keine rohen Backend-Antworten, Stacktraces, Tokens oder internen Adressen an Besucher ausgeben. Technische Details geh&ouml;ren in eine gesch&uuml;tzte Diagnoseansicht oder in bereinigte Logs.
Keine rohen Backend-Antworten, Stacktraces, Tokens oder internen Adressen an Besucher ausgeben. Technische Details gehören in eine geschützte Diagnoseansicht oder in bereinigte Logs.


== Zustands- und Bedienregeln ==
== Zustands- und Bedienregeln ==


* Mehrfachklicks verhindern, solange ein Request l&auml;uft.
* Mehrfachklicks verhindern, solange ein Request läuft.
* Erst nach erfolgreicher Antwort den n&auml;chsten fachlichen Zustand anzeigen.
* Erst nach erfolgreicher Antwort den nächsten fachlichen Zustand anzeigen.
* Ticketcodes und Pseudo-IDs nicht in der URL oder in dauerhaftem `localStorage` speichern.
* Ticketcodes und Pseudo-IDs nicht in der URL oder in dauerhaftem <code>localStorage</code> speichern. Für eine nur bis zum Schließen des Tabs benötigte Sitzung ist <code>sessionStorage</code> geeigneter; noch besser ist flüchtiger JavaScript-Zustand.
* Bei Seitenneuladung `/verify` verwenden, bevor eine bestehende Sitzung angenommen wird.
* Ticketcodes, Pseudo-IDs und vollständige authentifizierte Request-URLs nicht mit <code>console.log</code> oder in öffentlich zugängliche Telemetrie schreiben.
* Eine Station erst als verbunden anzeigen, wenn `/media-station` erfolgreich war.
* Bei Seitenneuladung <code>/verify</code> verwenden, bevor eine bestehende Sitzung angenommen wird.
* Bei `401` nicht automatisch einen falschen Code behaupten; auch Belegung und Projektzuordnung sind m&ouml;gliche Ursachen.
* Eine Station erst als verbunden anzeigen, wenn <code>/media-station</code> erfolgreich war.
* Bei <code>401</code> nicht automatisch einen falschen Code behaupten; auch Belegung und Projektzuordnung sind mögliche Ursachen.
* Bei unbekanntem Stationsstatus eine neutrale Fehlermeldung anzeigen.
* Bei unbekanntem Stationsstatus eine neutrale Fehlermeldung anzeigen.
* Bedienoberfl&auml;che tastaturbedienbar gestalten und Statusmeldungen mit `aria-live` ausgeben.
* Bedienoberfläche tastaturbedienbar gestalten und Statusmeldungen mit <code>aria-live</code> ausgeben.
* Stationsbefehle nur aus einer festen projektspezifischen Positivliste erzeugen, nicht aus frei eingegebenem Besuchertest.
* Stationsbefehle nur aus einer festen projektspezifischen Positivliste erzeugen, nicht aus frei eingegebenem Besuchertest.


=== Ticketcode aus der URL entfernen ===
Wird ein Ticketcode per QR-Link als Queryparameter übergeben, soll das Frontend ihn nach erfolgreicher Übernahme sofort aus der sichtbaren URL und der aktuellen Browserhistorie entfernen:
<pre>
const cleanUrl = new URL(window.location.href);
cleanUrl.searchParams.delete("systemCode");
window.history.replaceState({}, "", cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);
</pre>
Dadurch wird der Code nicht unnötig in kopierten URLs oder späteren Screenshots weitergegeben. Webserver- und Proxy-Logs können den ursprünglichen Request trotzdem bereits enthalten; Ticketcodes bleiben deshalb kurzlebige Einmalwerte.
Bei Frontends mit Hash-Routing, zum Beispiel <code>/#/welcome?systemCode=123456</code>, liegt der Parameter nicht in <code>window.location.search</code>, sondern innerhalb des Hash-Fragments. Er muss dort separat entfernt werden. Das verwendete Router-Framework soll nach erfolgreicher Übernahme mit einer ersetzenden Navigation (<code>replace</code>, nicht <code>push</code>) auf dieselbe Route ohne <code>systemCode</code> wechseln. So bleibt der Code auch nicht über die Zurück-Funktion erreichbar. Ein Hash-Fragment wird zwar nicht an den Webserver übertragen, bleibt aber in Browserhistorie, kopierten Links und Screenshots sichtbar.
== CORS und Hosting ==
== CORS und Hosting ==


Der aktuelle MegBes-Stand erlaubt CORS-Anfragen grunds&auml;tzlich. Das ist keine Zusage, dass jede zuk&uuml;nftige Instanz beliebige Origins akzeptiert. Bevorzugt werden Frontend und API unter kontrollierten HTTPS-Adressen betrieben. Ein CORS-Fehler wird nicht durch Abschalten von Browsersicherheit oder Zertifikatspr&uuml;fung umgangen.
Der aktuelle MegBes-Stand erlaubt CORS-Anfragen grundsätzlich. Das ist keine Zusage, dass jede zukünftige Instanz beliebige Origins akzeptiert. Bevorzugt werden Frontend und API unter kontrollierten HTTPS-Adressen betrieben. Ein CORS-Fehler wird nicht durch Abschalten von Browsersicherheit oder Zertifikatsprüfung umgangen.
 
Bei Single-Page-Anwendungen müssen öffentlicher Basispfad, Router-Basis und Asset-Pfade zur tatsächlichen Bereitstellung passen. Eine Anwendung, die beispielsweise für <code>/projekt/</code> gebaut wurde, ist ohne Neubau oder passende Serverkonfiguration nicht automatisch unter <code>/</code> oder einem anderen Unterpfad lauffähig. Direkte Aufrufe von Unterseiten benötigen außerdem eine passende Fallback-Regel auf <code>index.html</code>.
 
Zum Projektarchiv gehören neben den ausgelieferten minifizierten Dateien auch Quellcode, Paketmanifest, Lockdatei, Build-Anweisung und verwendete Laufzeitversionen. Nur ein Build-Ordner reicht nicht aus, um das Frontend später reproduzierbar zu warten oder sicher zu aktualisieren.
 
== Bestätigter Referenztest ==
 
Am 6. August 2026 wurde der vollständige Ablauf gegen das interne Projekt <code>tine</code> geprüft:
 
Frontend-Aufruf → megbes-external → megbes-internal → UDP-Versand
 
<code>enter-system</code>, <code>verify</code>, Stationsanmeldung, <code>video-command</code> mit <code>start</code> und <code>stop</code>, Stationsabmeldung sowie erneutes <code>verify</code> antworteten jeweils mit <code>200 OK</code>. <code>megbes-internal</code> bestätigte den UDP-Versand beider Befehle. Die Statusabfrage antwortete erwartungsgemäß mit <code>404</code>, da <code>tine</code> nicht im intern stationsgesteuerten Modus betrieben wird.
 
Der Test bestätigt den Signalweg bis zum UDP-Versand. Der tatsächliche Empfang und die Reaktion eines Endgeräts müssen bei der Projektabnahme separat geprüft werden. Konkrete IP-Adressen, Ticketcodes und Geheimwerte werden nicht im Wiki festgehalten.


Eine ergänzende statische Prüfung des umfangreicheren Ionic-/Vue-Frontends des Projekts <code>gkp</code> bestätigte denselben grundlegenden Vertrag: Aktivierung über <code>/enter-system</code>, Prüfung über <code>/verify</code>, Stationsanmeldung und optionaler Stationsstatus sowie Befehle über <code>/video-command</code> mit dem konfigurierten Authentifizierungsheader. Damit ist der hier beschriebene Ablauf nicht nur mit der Minimalvorlage, sondern auch mit einem realen mehrsprachigen Projektfrontend konsistent. Diese Prüfung bestätigt die API-Verwendung im Frontend; sie ersetzt keinen End-to-End-Test der jeweiligen Installation.
== Entwicklungs- und Abnahmetests ==
== Entwicklungs- und Abnahmetests ==


# `GET /info` von der vorgesehenen Hardware aufrufen.
# <code>GET /info</code> von der vorgesehenen Hardware aufrufen.
# korrekten Sicherheits- und Stationsmodus best&auml;tigen.
# korrekten Sicherheits- und Stationsmodus bestätigen.
# g&uuml;ltigen, ung&uuml;ltigen, bereits benutzten und abgelaufenen Ticketcode testen.
# gültigen, ungültigen, bereits benutzten und abgelaufenen Ticketcode testen.
# freie, belegte und unbekannte Station testen.
# freie, belegte und unbekannte Station testen.
# jeden freigegebenen Stationsbefehl einzeln pr&uuml;fen.
# jeden freigegebenen Stationsbefehl einzeln prüfen.
# Mehrfachklick und parallele Requests testen.
# Mehrfachklick und parallele Requests testen.
# Abmeldung und erneute Anmeldung testen.
# Abmeldung und erneute Anmeldung testen.
Zeile 476: Zeile 528:
== Grenzen dieser Referenz ==
== Grenzen dieser Referenz ==


Diese Seite dokumentiert den API-Vertrag des aktuellen Repository-Stands. Projektspezifische Stationsbefehle, Gestaltung, Inhalte, Ticketdruck, Hardware-Autostart und konkrete Frontend-Dateipfade werden auf der jeweiligen Projektseite dokumentiert. Bei Abweichungen hat der tats&auml;chlich eingesetzte Release Vorrang.
Diese Seite dokumentiert den API-Vertrag des aktuellen Repository-Stands. Projektspezifische Stationsbefehle, Gestaltung, Inhalte, Ticketdruck, Hardware-Autostart und konkrete Frontend-Dateipfade werden auf der jeweiligen Projektseite dokumentiert. Bei Abweichungen hat der tatsächlich eingesetzte Release Vorrang.


Siehe auch [[Web-Frontend]], [[Bedienung und Authentifizierung]], [[UDP und Stationszust&auml;nde]], [[Client-Feedback und Stationsmeldungen]] und [[Routing und Signalweg]].
Siehe auch [[Web-Frontend]], [[Bedienung und Authentifizierung]], [[UDP und Stationszustände]], [[Client-Feedback und Stationsmeldungen]] und [[Routing und Signalweg]].


[[Kategorie:MegBes]]
[[Kategorie:MegBes]]

Aktuelle Version vom 6. August 2026, 20:50 Uhr

Web-Frontend entwickeln

[Bearbeiten]

Zweck und Geltungsbereich

[Bearbeiten]

Diese Seite beschreibt die HTML-/JavaScript-Anbindung eines Web-Frontends an megbes-external. Sie basiert auf den Controllern und Request-Modellen des aktuellen MegBes-Repositorys. Vor der Freigabe eines Frontends muss trotzdem geprüft werden, welcher MegBes-Release im betreffenden Projekt installiert ist.

Das Frontend spricht ausschließlich megbes-external an. Direkte Browseraufrufe an megbes-internal sind nicht vorgesehen.

Browser → HTTPS → megbes-external → megbes-internal → UDP → Station

Vor Beginn festlegen

[Bearbeiten]
Wert Bedeutung
API-Basisadresse HTTPS-Adresse der projektspezifischen External-Instanz
Projektkennung Wert für prSc; gegen den eingesetzten Release und die Projektkonfiguration prüfen
Sicherheitsmodus ohne Authentifizierung oder zentrale Ticket-/Benutzerauthentifizierung
Stationssteuerung extern oder durch megbes-internal kontrolliert
Token-Header Wert der Laufzeitkonfiguration jwt.header; nicht pauschal Authorization annehmen
Stationsnummern freigegebene Stationen des Projekts
Stationsbefehle projektspezifische Befehle, die die jeweilige Station tatsächlich versteht

Domains, Headername, Projektkennung, Stationsnummern und Befehle dürfen nicht ungeprüft von einem anderen Projekt übernommen werden.

Betriebsarten und Stationsstatus

[Bearbeiten]
Betriebsart Konfiguration Verhalten des Frontends
extern verwaltete Stationsbelegung megbes.internal.station.controlled=false Station über GET /media-station?station=... belegen; den Status-Endpunkt nicht aufrufen
durch megbes-internal kontrollierte Station megbes.internal.station.controlled=true nach der Stationsanmeldung zusätzlich GET /media-station/status/{stationNumber} auswerten

Liefert der Status-Endpunkt 404 Not Found, obwohl Anmeldung und Stationsbelegung funktionieren, ist zuerst die Betriebsart zu prüfen. Bei extern verwalteter Stationsbelegung ist 404 erwartbar und bedeutet nicht, dass die Station belegt oder gestört ist. Die Betriebsart soll als zentraler Projektwert gepflegt und nicht aus einer fehlgeschlagenen Statusabfrage erraten werden.

API-Übersicht

[Bearbeiten]
Methode und Pfad Zweck Berechtigung Erfolgsantwort
GET /info vom Server empfangenen User-Agent prüfen frei 200 mit User-Agent
POST /authenticate Ticket-Monitor administrativ authentifizieren frei; benötigt Header customer und authId 200 und { "id": "..." }
GET /verify-admin Administrationskennung prüfen ADMIN 200
GET /ticket Ticketcode erzeugen oder vorhandenen unbenutzten Code abrufen ADMIN 200 und { "id": "......" }
POST /enter-system Ticketcode einmalig aktivieren frei 200
GET /verify aktivierte Benutzerkennung prüfen USER oder ADMIN 200
GET /media-station?station=... Benutzer an Station anmelden USER oder ADMIN 200
GET /media-station/status/{stationNumber} Stationszustand abrufen USER oder ADMIN; nur bei intern kontrollierten Stationen 200 und { "stationStatus": "..." }
POST /video-command einen oder mehrere UDP-Befehle senden ohne Security frei, sonst USER oder ADMIN 200 mit leerem Antwortkörper
DELETE /media-station Benutzer abmelden und Station zurücksetzen USER oder ADMIN 200

Wichtige Authentifizierungsregel

[Bearbeiten]

MegBes liefert dem Browser beziehungsweise Ticket-Monitor eine Pseudo-ID. Der eigentliche JWT verbleibt serverseitig. Bei geschützten Folgeaufrufen sendet das Frontend die Pseudo-ID als Wert des projektspezifisch konfigurierten Token-Headers.

<konfigurierter-token-header>: <Pseudo-ID>

Der Headername stammt aus jwt.header. Er darf im Frontend nicht geraten werden. Die Pseudo-ID ist wie eine Sitzungskennung zu behandeln: nicht protokollieren, nicht in URLs anhängen und nicht ins Wiki kopieren.

Request-Modelle

[Bearbeiten]

System betreten

[Bearbeiten]

POST /enter-system

{
  "systemCode": "123456"
}

systemCode darf nicht leer sein. Ein unbekannter oder bereits verwendeter Code ergibt 401 Unauthorized.

Stationsbefehl

[Bearbeiten]

POST /video-command

{
  "prSc": "<Projektkennung>",
  "statNoCommands": [
    "12-play"
  ]
}

Regeln des aktuellen Request-Modells:

  • prSc ist Pflicht und darf nicht leer sein.
  • statNoCommands ist eine nicht leere Liste.
  • Jeder Listeneintrag folgt \d{0,3}-[0-9a-zA-Z]{2,20}.
  • Vor dem Bindestrich sind damit null bis drei Ziffern zulässig; für produktive Stationsbefehle soll eine eindeutige, im Projekt dokumentierte Stationsnummer verwendet werden.
  • Nach dem Bindestrich sind zwei bis zwanzig ASCII-Buchstaben oder Ziffern zulässig.
  • Ob ein formal gültiger Befehl fachlich unterstützt wird, entscheidet die jeweilige Stationssoftware.

Beispiele wie 12-play sind nur Formbeispiele und keine globale MegBes-Befehlsliste.

Empfohlener JavaScript-API-Client

[Bearbeiten]

Die Netzwerkanbindung soll an einer Stelle gekapselt werden. Dadurch bleiben Fehlerbehandlung, Header und JSON-Verarbeitung konsistent.

const megBesConfig = Object.freeze({
  apiBase: "https://<projekt>.megbes.de",
  projectCode: "<PROJEKTKENNUNG>",
  tokenHeader: "<WERT-AUS-jwt.header>"
});

class MegBesError extends Error {
  constructor(message, status, responseBody) {
    super(message);
    this.name = "MegBesError";
    this.status = status;
    this.responseBody = responseBody;
  }
}

async function megBesRequest(path, {
  method = "GET",
  token = null,
  body = undefined,
  headers = {}
} = {}) {
  const requestHeaders = new Headers(headers);

  if (token) {
    requestHeaders.set(megBesConfig.tokenHeader, token);
  }
  if (body !== undefined) {
    requestHeaders.set("Content-Type", "application/json");
  }

  let response;
  try {
    response = await fetch(`${megBesConfig.apiBase}${path}`, {
      method,
      headers: requestHeaders,
      body: body === undefined ? undefined : JSON.stringify(body),
      cache: "no-store"
    });
  } catch (cause) {
    throw new MegBesError("MegBes ist nicht erreichbar.", 0, cause);
  }

  const contentType = response.headers.get("content-type") || "";
  const responseBody = contentType.includes("application/json")
    ? await response.json()
    : await response.text();

  if (!response.ok) {
    throw new MegBesError(
      `MegBes-Aufruf fehlgeschlagen (${response.status}).`,
      response.status,
      responseBody
    );
  }

  return responseBody;
}

Variante A: Frontend ohne Authentifizierung

[Bearbeiten]

Diese Variante existiert nur, wenn megbes.security für die External-Instanz nicht aktiviert ist. Das Frontend darf diesen Modus nicht anhand eines fehlgeschlagenen Loginversuchs erraten; er wird vor der Entwicklung projektspezifisch festgelegt.

async function sendCommandWithoutAuthentication(station, command) {
  return megBesRequest("/video-command", {
    method: "POST",
    body: {
      prSc: megBesConfig.projectCode,
      statNoCommands: [`${station}-${command}`]
    }
  });
}

Auch ohne MegBes-Authentifizierung muss die External-Instanz ausschließlich über HTTPS angesprochen werden. Welche Personen die Seite erreichen dürfen, ist außerhalb von MegBes abzusichern.

Variante B: Ticket-Monitor

[Bearbeiten]

1. Ticket-Monitor authentifizieren

[Bearbeiten]

Der Ticket-Monitor ruft POST /authenticate mit den Headern customer und authId auf. Zusätzlich prüft MegBes den User-Agent gegen die Projektkonfiguration.

async function authenticateTicketMonitor(customer, authId) {
  const response = await megBesRequest("/authenticate", {
    method: "POST",
    headers: {
      customer,
      authId
    }
  });
  return response.id;
}

authId ist ein Geheimwert und darf nicht fest in ausgelieferter Browser-JavaScript-Datei stehen. Wenn der Ticket-Monitor nur statische öffentliche Dateien ausliefert, ist dieses Verfahren sicherheitlich neu zu bewerten. Die Kennung gehört in eine geschützte Laufzeitumgebung oder einen vertrauenswürdigen lokalen Dienst.

2. Ticketcode abrufen

[Bearbeiten]
async function getTicket(adminToken, customer, ticketMonitorNumber) {
  const query = new URLSearchParams({
    customer,
    ticketMonitorNumber: String(ticketMonitorNumber)
  });

  const response = await megBesRequest(`/ticket?${query}`, {
    token: adminToken
  });
  return response.id;
}

Der gelieferte Code ist im aktuellen Stand sechsstellig. Das Frontend soll ihn als String behandeln, damit eine führende Null nicht verloren geht.

Variante C: Besucher- oder Guide-Frontend mit Ticketcode

[Bearbeiten]

1. Code aktivieren

[Bearbeiten]
async function enterSystem(systemCode) {
  await megBesRequest("/enter-system", {
    method: "POST",
    body: { systemCode }
  });
  return systemCode;
}

Nach erfolgreichem Eintritt dient derselbe Code als Pseudo-ID für die weiteren Aufrufe. Ein Ticketcode kann nur einmal aktiviert werden.

2. Kennung prüfen

[Bearbeiten]
async function verifyUser(userToken) {
  await megBesRequest("/verify", { token: userToken });
  return true;
}

3. An Station anmelden

[Bearbeiten]
async function loginStation(userToken, stationNumber) {
  const query = new URLSearchParams({ station: String(stationNumber) });
  await megBesRequest(`/media-station?${query}`, { token: userToken });
}

Im intern kontrollierten Modus ist die Stationsnummer zwingend. Ein 401 kann neben einer ungültigen Kennung auch bedeuten, dass die Station belegt ist oder die Stationsanmeldung abgelehnt wurde. Die Bedienoberfläche darf deshalb nicht automatisch behaupten, der Ticketcode sei falsch.

4. Optional Stationsstatus prüfen

[Bearbeiten]

Dieser Endpunkt ist nur verfügbar, wenn megbes.internal.station.controlled=true gilt.

async function getStationStatus(userToken, stationNumber) {
  const response = await megBesRequest(
    `/media-station/status/${encodeURIComponent(stationNumber)}`,
    { token: userToken }
  );
  return response.stationStatus;
}

Der aktuelle Code verwendet unter anderem die Zustände OPEN, IDLE, REMOTE und TOUCHSCREEN. Die Oberfläche soll unbekannte künftige Werte neutral anzeigen und nicht abstürzen.

5. Befehl senden

[Bearbeiten]
async function sendCommand(userToken, stationNumber, command) {
  return megBesRequest("/video-command", {
    method: "POST",
    token: userToken,
    body: {
      prSc: megBesConfig.projectCode,
      statNoCommands: [`${stationNumber}-${command}`]
    }
  });
}

Der erfolgreiche Aufruf antwortet derzeit mit 200 OK und leerem Body. JavaScript-Clients dürfen deshalb nicht zwingend JSON parsen. Bei jQuery ist insbesondere dataType: 'json' für diesen Aufruf ungeeignet. Eine Navigation oder Erfolgsmeldung darf nur im Erfolgszweig (.done() beziehungsweise response.ok) erfolgen, nicht in .always() oder finally.

6. Station abmelden

[Bearbeiten]
async function logoutStation(userToken) {
  await megBesRequest("/media-station", {
    method: "DELETE",
    token: userToken
  });
}

Die Abmeldung soll über eine sichtbare Bedienhandlung und bei regulärem Abschluss erfolgen. Ein Aufruf in beforeunload ist allein nicht zuverlässig, da Browser ausstehende Requests beim Schließen abbrechen können. Der projektspezifische Stations-Timer bleibt daher als Rückfallebene erforderlich.

DELETE /media-station gibt nur die aktuell belegte Station frei und setzt deren Stationszustand zurück. Die Ticket- beziehungsweise Benutzersitzung bleibt gültig; ein anschließendes GET /verify kann daher weiterhin 200 OK liefern. So kann derselbe Besucher danach eine andere Station auswählen.

Vollständiger Ablauf eines Besucher-Frontends

[Bearbeiten]
let userToken = null;
let activeStation = null;

async function startSession(systemCode, stationNumber) {
  userToken = await enterSystem(systemCode);
  await verifyUser(userToken);

  // Nur im intern kontrollierten Modus vorab verwenden.
  // const status = await getStationStatus(userToken, stationNumber);

  await loginStation(userToken, stationNumber);
  activeStation = String(stationNumber);
}

async function play(command) {
  if (!userToken || !activeStation) {
    throw new Error("Keine aktive MegBes-Station.");
  }
  await sendCommand(userToken, activeStation, command);
}

async function endSession() {
  if (userToken && activeStation) {
    await logoutStation(userToken);
  }
  userToken = null;
  activeStation = null;
}

Minimales HTML-Beispiel

[Bearbeiten]

Das Beispiel zeigt die Struktur, ist aber ohne projektspezifische Werte und Gestaltung nicht produktionsfertig.

<!doctype html>
<html lang="de">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>MegBes-Steuerung</title>
</head>
<body>
  <main>
    <form id="login-form">
      <label for="code">Ticketcode</label>
      <input id="code" name="code" inputmode="numeric" autocomplete="one-time-code" required>

      <label for="station">Station</label>
      <input id="station" name="station" inputmode="numeric" required>

      <button type="submit">Station verbinden</button>
    </form>

    <section id="controls" hidden>
      <button type="button" data-command="play">Start</button>
      <button type="button" data-command="stop">Stopp</button>
      <button type="button" id="logout">Beenden</button>
    </section>

    <p id="message" role="status" aria-live="polite"></p>
  </main>

  <script src="megbes-api.js"></script>
  <script>
    const form = document.querySelector("#login-form");
    const controls = document.querySelector("#controls");
    const message = document.querySelector("#message");

    form.addEventListener("submit", async event => {
      event.preventDefault();
      message.textContent = "Verbindung wird hergestellt ...";
      try {
        await startSession(form.code.value.trim(), form.station.value.trim());
        controls.hidden = false;
        message.textContent = "Station ist verbunden.";
      } catch (error) {
        message.textContent = messageForMegBesError(error);
      }
    });

    controls.addEventListener("click", async event => {
      const command = event.target.dataset.command;
      if (!command) return;
      try {
        event.target.disabled = true;
        await play(command);
        message.textContent = "Befehl wurde gesendet.";
      } catch (error) {
        message.textContent = messageForMegBesError(error);
      } finally {
        event.target.disabled = false;
      }
    });

    document.querySelector("#logout").addEventListener("click", async () => {
      try {
        await endSession();
        controls.hidden = true;
        form.reset();
        message.textContent = "Sitzung wurde beendet.";
      } catch (error) {
        message.textContent = messageForMegBesError(error);
      }
    });
  </script>
</body>
</html>

Die sichtbaren Befehle play und stop sind ebenfalls nur Platzhalter. Sie werden durch die freigegebenen Projektbefehle ersetzt.

Fehlerbehandlung

[Bearbeiten]
function messageForMegBesError(error) {
  if (!(error instanceof MegBesError)) {
    return "Unerwarteter Fehler. Bitte Aufsicht informieren.";
  }

  switch (error.status) {
    case 0:
      return "MegBes ist derzeit nicht erreichbar.";
    case 400:
      return "Die Anfrage ist unvollständig oder ungültig.";
    case 401:
      return "Anmeldung, Berechtigung oder Stationsbelegung konnte nicht bestätigt werden.";
    case 403:
      return "Die Station ist für diese Sitzung nicht freigegeben.";
    case 404:
      return "Diese Funktion ist in der aktuellen Betriebsart nicht verfügbar.";
    default:
      return "MegBes konnte den Auftrag nicht ausführen.";
  }
}

Keine rohen Backend-Antworten, Stacktraces, Tokens oder internen Adressen an Besucher ausgeben. Technische Details gehören in eine geschützte Diagnoseansicht oder in bereinigte Logs.

Zustands- und Bedienregeln

[Bearbeiten]
  • Mehrfachklicks verhindern, solange ein Request läuft.
  • Erst nach erfolgreicher Antwort den nächsten fachlichen Zustand anzeigen.
  • Ticketcodes und Pseudo-IDs nicht in der URL oder in dauerhaftem localStorage speichern. Für eine nur bis zum Schließen des Tabs benötigte Sitzung ist sessionStorage geeigneter; noch besser ist flüchtiger JavaScript-Zustand.
  • Ticketcodes, Pseudo-IDs und vollständige authentifizierte Request-URLs nicht mit console.log oder in öffentlich zugängliche Telemetrie schreiben.
  • Bei Seitenneuladung /verify verwenden, bevor eine bestehende Sitzung angenommen wird.
  • Eine Station erst als verbunden anzeigen, wenn /media-station erfolgreich war.
  • Bei 401 nicht automatisch einen falschen Code behaupten; auch Belegung und Projektzuordnung sind mögliche Ursachen.
  • Bei unbekanntem Stationsstatus eine neutrale Fehlermeldung anzeigen.
  • Bedienoberfläche tastaturbedienbar gestalten und Statusmeldungen mit aria-live ausgeben.
  • Stationsbefehle nur aus einer festen projektspezifischen Positivliste erzeugen, nicht aus frei eingegebenem Besuchertest.

Ticketcode aus der URL entfernen

[Bearbeiten]

Wird ein Ticketcode per QR-Link als Queryparameter übergeben, soll das Frontend ihn nach erfolgreicher Übernahme sofort aus der sichtbaren URL und der aktuellen Browserhistorie entfernen:

const cleanUrl = new URL(window.location.href);
cleanUrl.searchParams.delete("systemCode");
window.history.replaceState({}, "", cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);

Dadurch wird der Code nicht unnötig in kopierten URLs oder späteren Screenshots weitergegeben. Webserver- und Proxy-Logs können den ursprünglichen Request trotzdem bereits enthalten; Ticketcodes bleiben deshalb kurzlebige Einmalwerte.

Bei Frontends mit Hash-Routing, zum Beispiel /#/welcome?systemCode=123456, liegt der Parameter nicht in window.location.search, sondern innerhalb des Hash-Fragments. Er muss dort separat entfernt werden. Das verwendete Router-Framework soll nach erfolgreicher Übernahme mit einer ersetzenden Navigation (replace, nicht push) auf dieselbe Route ohne systemCode wechseln. So bleibt der Code auch nicht über die Zurück-Funktion erreichbar. Ein Hash-Fragment wird zwar nicht an den Webserver übertragen, bleibt aber in Browserhistorie, kopierten Links und Screenshots sichtbar.

CORS und Hosting

[Bearbeiten]

Der aktuelle MegBes-Stand erlaubt CORS-Anfragen grundsätzlich. Das ist keine Zusage, dass jede zukünftige Instanz beliebige Origins akzeptiert. Bevorzugt werden Frontend und API unter kontrollierten HTTPS-Adressen betrieben. Ein CORS-Fehler wird nicht durch Abschalten von Browsersicherheit oder Zertifikatsprüfung umgangen.

Bei Single-Page-Anwendungen müssen öffentlicher Basispfad, Router-Basis und Asset-Pfade zur tatsächlichen Bereitstellung passen. Eine Anwendung, die beispielsweise für /projekt/ gebaut wurde, ist ohne Neubau oder passende Serverkonfiguration nicht automatisch unter / oder einem anderen Unterpfad lauffähig. Direkte Aufrufe von Unterseiten benötigen außerdem eine passende Fallback-Regel auf index.html.

Zum Projektarchiv gehören neben den ausgelieferten minifizierten Dateien auch Quellcode, Paketmanifest, Lockdatei, Build-Anweisung und verwendete Laufzeitversionen. Nur ein Build-Ordner reicht nicht aus, um das Frontend später reproduzierbar zu warten oder sicher zu aktualisieren.

Bestätigter Referenztest

[Bearbeiten]

Am 6. August 2026 wurde der vollständige Ablauf gegen das interne Projekt tine geprüft:

Frontend-Aufruf → megbes-external → megbes-internal → UDP-Versand

enter-system, verify, Stationsanmeldung, video-command mit start und stop, Stationsabmeldung sowie erneutes verify antworteten jeweils mit 200 OK. megbes-internal bestätigte den UDP-Versand beider Befehle. Die Statusabfrage antwortete erwartungsgemäß mit 404, da tine nicht im intern stationsgesteuerten Modus betrieben wird.

Der Test bestätigt den Signalweg bis zum UDP-Versand. Der tatsächliche Empfang und die Reaktion eines Endgeräts müssen bei der Projektabnahme separat geprüft werden. Konkrete IP-Adressen, Ticketcodes und Geheimwerte werden nicht im Wiki festgehalten.

Eine ergänzende statische Prüfung des umfangreicheren Ionic-/Vue-Frontends des Projekts gkp bestätigte denselben grundlegenden Vertrag: Aktivierung über /enter-system, Prüfung über /verify, Stationsanmeldung und optionaler Stationsstatus sowie Befehle über /video-command mit dem konfigurierten Authentifizierungsheader. Damit ist der hier beschriebene Ablauf nicht nur mit der Minimalvorlage, sondern auch mit einem realen mehrsprachigen Projektfrontend konsistent. Diese Prüfung bestätigt die API-Verwendung im Frontend; sie ersetzt keinen End-to-End-Test der jeweiligen Installation.

Entwicklungs- und Abnahmetests

[Bearbeiten]
  1. GET /info von der vorgesehenen Hardware aufrufen.
  2. korrekten Sicherheits- und Stationsmodus bestätigen.
  3. gültigen, ungültigen, bereits benutzten und abgelaufenen Ticketcode testen.
  4. freie, belegte und unbekannte Station testen.
  5. jeden freigegebenen Stationsbefehl einzeln prüfen.
  6. Mehrfachklick und parallele Requests testen.
  7. Abmeldung und erneute Anmeldung testen.
  8. Browser-Neuladen sowie Neustart von Frontend-Hardware und Station testen.
  9. Ausfall von External, Internal und Stationsnetz jeweils getrennt simulieren.
  10. sicherstellen, dass keine Tokens oder Geheimwerte in URL, Konsole, Screenshot oder sichtbarer Fehlermeldung erscheinen.
  11. installierten External-/Internal-Release, Frontend-Version und Testergebnis auf der Projektseite dokumentieren.

Grenzen dieser Referenz

[Bearbeiten]

Diese Seite dokumentiert den API-Vertrag des aktuellen Repository-Stands. Projektspezifische Stationsbefehle, Gestaltung, Inhalte, Ticketdruck, Hardware-Autostart und konkrete Frontend-Dateipfade werden auf der jeweiligen Projektseite dokumentiert. Bei Abweichungen hat der tatsächlich eingesetzte Release Vorrang.

Siehe auch Web-Frontend, Bedienung und Authentifizierung, UDP und Stationszustände, Client-Feedback und Stationsmeldungen und Routing und Signalweg.