joetjen.net
EN DE
Web-Framework

Die Demo-Anwendung

WebGlue wächst zusammen mit einer Begleitanwendung: einem kleinen Ticketsystem, das jede Bibliothek nutzt, sobald sie da ist — eine Seite im Browser, ein Socket, der von jeder Änderung erfährt, und dieselben Tickets als Text, DXN oder JSON.

In Entwicklung · v0.8

Aufbau

TeilInhalt
libs/ticketsDie Domäne — Tickets, das Store-Behaviour und sein In-Memory-Store, das Audit-Protokoll — ohne jedes HTTP
apps/webRouten, Handler, Middleware und der Supervision-Baum
apps/web/templatesDie Seite und die Ticket-Zeile, als HTML-Templates
prx webglue watchEin in Praxis geschriebener WebSocket-Client, um Pushes ankommen zu sehen

Die Domäne

Ein Ticket ist eine Map, und jeder Übergang liefert #(:ok ticket) oder #(:error reason). Ein Ticket wandert von offen zu zugewiesen, zwischen zugewiesen und pausiert hin und her und schließlich zu geschlossen. Jede Änderung landet in einem Protokoll — wer was wann getan hat —, unter dem Verb, das geschah, nicht dem Status, der daraus folgte.

libs/tickets/src/webglue/demo/ticket.prax
(defn assign [ticket who] :spec [map any -> tuple]
  (if (open? ticket)
      #(:ok (map/merge ticket {:status :assigned :assignee who}))
      #(:error :closed)))

Die HTTP-Seite

Die Teile sind die aus den Kernkonzepten: die Routing-Tabelle, die Anwendungsfunktion, der Supervision-Baum, eine Seite aus einem Template, Content-Negotiation beim Lesen, Validierung beim Anlegen und Suchen, ein Socket mit Presence und ein Push bei jeder Änderung. Ein abgelehnter Übergang wird zu 409, eine fehlende Anmeldung zu 401, ein unbekanntes Ticket zu 404, ein unerfüllbares Accept zu 406.

GET  /                    the page: every ticket, and a search form
GET  /assets/*            the files it ships
GET  /socket/tickets      a socket that is told what happens
POST /session             sign in;  DELETE /session  sign out
POST /tickets             raise one, anonymously
GET  /tickets             every ticket, or those ?q= and ?status= find
GET  /tickets/:id         one
GET  /tickets/:id/trail   who did what to it, and when
POST /tickets/:id/assign  take it
POST /tickets/:id/hold    wait for the reporter
POST /tickets/:id/resume  they answered
POST /tickets/:id/close   done

Eine Sitzung damit

$ prx run --no-halt webglue_demo     # WEBGLUE_ADAPTER=wrangler, WEBGLUE_PORT=4100
webglue_demo listening on http://localhost:4000

$ curl -d 'subject=Printer is on fire' localhost:4000/tickets
#1 [open] Printer is on fire

$ curl -X POST localhost:4000/tickets/1/assign
log in first                                         # 401

$ curl -X POST -H 'X-Demo-User: jan' localhost:4000/tickets/1/assign
#1 [assigned] Printer is on fire -> jan

$ curl -H 'Accept: application/dxn' localhost:4000/tickets/1
%{id:1,status::assigned,body:"",subject:"Printer is on fire",assignee:"jan",reporter:nil}

$ curl -H 'Accept: application/json' localhost:4000/tickets/1
{"id":1,"status":"assigned","body":"","subject":"Printer is on fire","assignee":"jan","reporter":null}

$ curl -H 'Accept: image/png' localhost:4000/tickets/1
none of what you accept; this answers text/plain, application/dxn, application/json   # 406

$ curl 'localhost:4000/tickets?q=printer'
#1 [assigned] Printer is on fire -> jan

$ curl 'localhost:4000/tickets?status=lost'
status is not one there is                           # 400

$ curl -X POST -H 'X-Demo-User: jan' localhost:4000/tickets/1/close
#1 [closed] Printer is on fire -> jan

$ curl -X POST -H 'X-Demo-User: jan' localhost:4000/tickets/1/assign
refused: closed                                      # 409

$ curl localhost:4000/tickets/1/trail
2026-10-19T20:01:05.053253Z anonymous raised
2026-10-19T20:01:05.173131Z jan assigned
2026-10-19T20:01:05.663730Z jan closed

GET / beantwortet dieselben Tickets als HTML-Seite, gerendert aus den Templates. X-Demo-User ist eine Abkürzung, die die Cookie-Session umgeht, damit curl keinen Cookie-Speicher braucht; die echte Anmeldung ist POST /session mit einem Feld user, und am gesetzten Cookie wird die nächste Anfrage erkannt.

Den Socket beobachten

prx webglue watch ist ein in Praxis geschriebener WebSocket-Client, der Handshake, Maskierung und Framing selbst erledigt. Vor dem Anlegen eines Tickets geöffnet, empfängt er die Begrüßung, einen Presence-Diff und dann beide Umschläge für das neue Ticket — die Daten und die Zeile als Markup-Baum, adressiert an #tickets. Der Socket schreibt DXN-Text, damit ein Mensch ihn lesen kann.

$ prx webglue watch
101 Switching Protocols, and the accept value checks out
<- %{type:"data",topic:"tickets",payload:%{watching:"tickets",whom:["anonymous"]}}
<- %{presence-diff:%{left:%{},joined:%{"anonymous"=>[%{}]}}}
<- %{type:"data",topic:"tickets",payload:%{ticket:%{id:1,status::open,body:"",subject:"Printer is on fire",assignee:nil,reporter:nil},event::raised}}
<- %{type:"html_fragment",element:{:li %{id:"ticket-1",class:"open"} {:span %{class:"id"} "#" "1"} {:span %{class:"subject"} "Printer is on fire"} nil},target:"#tickets"}
-> ping
<- %{type:"data",topic:"tickets",payload:%{pong:3}}

Was sie noch nicht tut

  • Keine Datenbank — der Store liegt im Speicher, hinter der Nahtstelle zur Datenschicht, und bleibt austauschbar.
  • Identität ist ein Name; niemand prüft ihn, und Autorisierung gibt es nicht.
  • Lesende Anfragen antworten mit Text, DXN oder JSON; eine Änderung antwortet weiterhin mit einer Textzeile.
  • Die Seite hat kein eigenes Skript — gepushte Fragmente werden noch nicht in sie eingesetzt; man beobachtet sie mit einem WebSocket-Client.