All projects Atrium · Write-up

Live tiles without a single cross-origin callLive-Kacheln ohne einen einzigen fremden Aufruf

Atrium puts the web apps on one wall, and each tile shows a live piece of its app: Clinch’s playoff seeds, today’s dish from the canteen, 90 days of uptime. The page never calls another app; its own small server asks them and answers with one document.Atrium stellt die Web-Apps auf eine Wand, und jede Kachel zeigt ein Stück ihrer App live: Clinchs Playoff-Seeds, das Gericht des Tages aus der Kantine, 90 Tage Verfügbarkeit. Die Seite ruft nie eine andere App auf; ihr eigener kleiner Server fragt sie und antwortet mit einem Dokument.

apps.ewolution.cloud Source

Type
Web appWeb-App
Stack
Svelte 5 · Node · View Transitions
Year
2026
Code
Public on GitHubÖffentlich auf GitHub

One origin, three apps

Three of the tiles show something live. The obvious build has the page fetch from each app: three more origins in the page’s Content Security Policy, CORS headers on three servers, and every visitor’s browser talking to every app. Atrium’s page talks only to its own origin. Its server, a Node process without dependencies, reads the three apps over the NAS’s own network and hands the page one summary at /api/apps. The CSP stays at 'self', and none of the apps had to change.

Clinch/api/snapshot, 76 KB. Atrium keeps seeds one to eight in each conference, enough for the cut line and the first team out. Read every 5 minutes.
Cantina/api/menu, 236 KB. Atrium keeps one dish: today’s main meal at one outlet. Read every 15 minutes.
Pulse/api/status, 16 KB. Atrium keeps each app’s state and its 90 days of uptime. Read every minute.

From Pulse, the summary carries only what Pulse’s own public page shows. The addresses Pulse checks are LAN addresses and stay on Pulse, and an upstream failure reaches Atrium’s log as a code, never as a message that could name a host.

Never waiting on a slow app

Each source keeps its last good answer. A page request doesn’t wait for a refresh: a source with an answer serves it and refreshes behind it. Only a source with nothing yet, right after a restart, holds the request, for at most 2.5 seconds. After that the tile renders without its data and fills in on the next poll. Requests that arrive together share one fetch instead of starting their own.

A failed refresh keeps the old answer and is retried after 30 seconds, so one bad minute upstream doesn’t stick. An answer three refresh intervals old is marked stale. The page asks every minute while it’s visible and keeps the last good copy on the device: offline, it opens with what it last saw, labelled “Last known” with its time once that’s more than three minutes old.

2.5 sThe longest a page waits for an app, and only when nothing is cached yet.
22 KBThe whole summary: every app, 90 days of uptime, two languages. 4.5 KB compressed.
0Requests from the page to any other origin. Fonts, icons and photos are served by Atrium.

One dish, one photo

Cantina’s menu covers five outlets, two weeks and two languages, and the tile has room for one dish. Atrium takes Elbe’s menu for today, or for the next day Elbe serves, so on a Saturday it shows Monday. It picks the first section with a priced dish, in the order The Original, Grill, F&T Vegan, Green, then any other: the hot main meals first. The tile shows the dish’s name in the page’s language, its price, its CO₂ rating and whether it’s vegan or vegetarian, and Open goes straight to that outlet and day in Cantina.

The photo needed care. Cantina already proxies the kitchen’s images, and Atrium’s CSP only allows its own origin, so the photo comes through Atrium as /api/cantina/photo?d=<dish>. That route serves exactly one image, the dish the tile shows right now. Any other id is a 404, and the path it fetches comes from Cantina’s own menu and must look like one of Cantina’s image paths, so the route can’t be turned into a proxy for anything else. The URL names the dish, so a new dish is a new URL, and a browser may keep each one for a day.

Status is Pulse’s

Every dot on the wall, the line under its title and the 90 ticks on an app’s page come from Pulse unchanged: a day is green when every check passed, red when none did, amber in between and grey without checks. The Pulse tile folds all the apps into one bar, each day taking the worst state any app had, next to how many are up right now. Pulse doesn’t check itself, so its own dot means one thing only: whether it answered Atrium in the last three minutes.

On an app’s page the ticks are Folio’s <ewo-ticks>, the shared element made from Pulse’s bar. Point at a day, drag across the bar on a phone or use the arrow keys, and the line underneath gives the date, the uptime and the average response time.

Apps only the owner sees

Census and Folio are on the list too, marked for the owner. They never reach a guest’s browser, not even their names: the server filters the list before it answers, the copy the page keeps on the device holds only the public apps, and the tests fail if a guest’s answer ever contains one of them. Signing in is built and switched off. Once it’s on, it works like this:

  1. Access guards one pathA Cloudflare Access application covers only /login. The rest of the site stays public, and Access never sees it.
  2. The server checks the token itselfAccess only guards the hostname, and the port on the NAS would skip it, so the server verifies Access’s signed token against the team’s published keys, as Census does.
  3. Then a cookie of its ownA valid sign-in gets a session cookie signed by Atrium, HttpOnly and Secure, for 30 days. A guest never gets a cookie at all.

The offline worker needed one exception. It answers page loads from the network and falls back to the cached page when that fails. /login leaves the site for Access’s sign-in page on another origin, a redirect the worker’s fetch can’t follow, so it would have shown the cached page instead. The worker leaves /login, /logout and Cloudflare’s own paths alone.

A page that stays still

A tile opens its app’s page, at /clinch, /cantina and so on: a page rather than an overlay, so it scrolls, shares and goes Back like any other. Where the browser has View Transitions, the tile grows into the page’s stage and shrinks back into its place on the way out; elsewhere the page simply changes.

Everything that moves follows the pointer. The dots behind the page are only drawn while the mouse moves and they settle, so an idle page renders no frames at all, and touch is ignored, since on a phone it means scrolling. Tiles animate only while hovered: Clinch’s brackets close, Cantina’s plate turns, Fermata’s waveform plays, and Planum’s floor plan tips into 3D, its walls drawn as three layers lifted 10, 20 and 30 pixels along the Z axis. While the page scrolls, hover pauses, so tiles sliding under a resting cursor don’t light up one after another.

Eine Adresse, drei Apps

Drei der Kacheln zeigen etwas live. Der naheliegende Weg wäre, dass die Seite bei jeder App selbst abfragt: drei weitere Origins in der Content Security Policy, CORS-Header auf drei Servern und der Browser jedes Besuchers im Gespräch mit jeder App. Atriums Seite spricht nur mit ihrer eigenen Adresse. Ihr Server, ein Node-Prozess ohne Abhängigkeiten, liest die drei Apps über das eigene Netz des NAS und gibt der Seite eine Zusammenfassung unter /api/apps. Die CSP bleibt bei 'self', und keine der Apps musste sich ändern.

Clinch/api/snapshot, 76 KB. Atrium behält die Seeds eins bis acht jeder Conference, genug für die Playoff-Grenze und das erste Team darunter. Alle 5 Minuten gelesen.
Cantina/api/menu, 236 KB. Atrium behält ein Gericht: das Hauptgericht des Tages an einem Standort. Alle 15 Minuten gelesen.
Pulse/api/status, 16 KB. Atrium behält den Zustand jeder App und ihre 90 Tage Verfügbarkeit. Jede Minute gelesen.

Von Pulse enthält die Zusammenfassung nur, was auch Pulses eigene öffentliche Seite zeigt. Die Adressen, die Pulse prüft, sind LAN-Adressen und bleiben bei Pulse, und ein Fehler bei einer App landet in Atriums Log als Code, nie als Meldung, die einen Host nennen könnte.

Nie auf eine langsame App warten

Jede Quelle behält ihre letzte gute Antwort. Eine Anfrage der Seite wartet nicht auf eine Aktualisierung: Eine Quelle mit Antwort liefert sie und aktualisiert dahinter. Nur eine Quelle, die noch nichts hat, direkt nach einem Neustart, hält die Anfrage auf, höchstens 2,5 Sekunden. Danach erscheint die Kachel ohne ihre Daten und füllt sich bei der nächsten Abfrage. Anfragen, die gleichzeitig kommen, teilen sich einen Abruf, statt jede einen eigenen zu starten.

Eine fehlgeschlagene Aktualisierung behält die alte Antwort und wird nach 30 Sekunden wiederholt, damit eine schlechte Minute bei einer App nicht hängen bleibt. Eine Antwort, die drei Abrufintervalle alt ist, gilt als veraltet. Die Seite fragt jede Minute, solange sie sichtbar ist, und behält die letzte gute Kopie auf dem Gerät: Offline öffnet sie mit dem, was sie zuletzt gesehen hat, und schreibt „Zuletzt bekannt“ mit Uhrzeit dazu, sobald das älter als drei Minuten ist.

2,5 sLänger wartet eine Seite nie auf eine App, und auch das nur, wenn noch nichts zwischengespeichert ist.
22 KBDie ganze Zusammenfassung: jede App, 90 Tage Verfügbarkeit, zwei Sprachen. Komprimiert 4,5 KB.
0Anfragen der Seite an eine andere Origin. Schriften, Icons und Fotos liefert Atrium selbst.

Ein Gericht, ein Foto

Cantinas Speiseplan umfasst fünf Standorte, zwei Wochen und zwei Sprachen, und die Kachel hat Platz für ein Gericht. Atrium nimmt den Plan von Elbe für heute oder für den nächsten Tag, an dem Elbe kocht, an einem Samstag also den Montag. Es nimmt die erste Station mit einem Gericht mit Preis, in der Reihenfolge The Original, Grill, F&T Vegan, Green, danach jede andere: die warmen Hauptgerichte zuerst. Die Kachel zeigt den Namen in der Sprache der Seite, den Preis, die CO₂-Bewertung und ob es vegan oder vegetarisch ist, und Öffnen führt direkt zu diesem Standort und Tag in Cantina.

Das Foto brauchte Sorgfalt. Cantina leitet die Bilder der Küche schon selbst durch, und Atriums CSP erlaubt nur die eigene Adresse, also kommt das Foto über Atrium als /api/cantina/photo?d=<gericht>. Diese Route liefert genau ein Bild: das des Gerichts, das die Kachel gerade zeigt. Jede andere ID ist ein 404, und der Pfad, den sie abruft, stammt aus Cantinas eigenem Speiseplan und muss wie einer von Cantinas Bildpfaden aussehen, sodass die Route nie zum Proxy für etwas anderes werden kann. Die URL nennt das Gericht, ein neues Gericht ist also eine neue URL, und ein Browser darf jede einen Tag lang behalten.

Der Status kommt von Pulse

Jeder Punkt auf der Wand, die Zeile unter ihrem Titel und die 90 Striche auf der Seite einer App kommen unverändert von Pulse: Ein Tag ist grün, wenn jede Prüfung bestanden wurde, rot, wenn keine bestanden wurde, gelb dazwischen und grau ohne Prüfungen. Die Pulse-Kachel fasst alle Apps in einem Balken zusammen, jeder Tag mit dem schlechtesten Zustand, den eine App hatte, daneben, wie viele gerade erreichbar sind. Pulse prüft sich nicht selbst, also bedeutet sein eigener Punkt nur eins: ob es Atrium in den letzten drei Minuten geantwortet hat.

Auf der Seite einer App sind die Striche Folios <ewo-ticks>, das gemeinsame Element, das aus Pulses Balken entstanden ist. Auf einen Tag zeigen, auf dem Handy über den Balken ziehen oder die Pfeiltasten nutzen, und die Zeile darunter nennt Datum, Verfügbarkeit und mittlere Antwortzeit.

Apps, die nur der Besitzer sieht

Census und Folio stehen auch auf der Liste, markiert für den Besitzer. Sie erreichen nie den Browser eines Gastes, nicht einmal ihre Namen: Der Server filtert die Liste, bevor er antwortet, die Kopie auf dem Gerät enthält nur die öffentlichen Apps, und die Tests schlagen fehl, sobald die Antwort an einen Gast eine von ihnen enthält. Die Anmeldung ist gebaut und ausgeschaltet. Eingeschaltet funktioniert sie so:

  1. Access schützt einen PfadEine Cloudflare-Access-Anwendung deckt nur /login ab. Der Rest der Seite bleibt öffentlich, und Access bekommt ihn nie zu sehen.
  2. Der Server prüft das Token selbstAccess schützt nur den Hostnamen, und der Port auf dem NAS würde es umgehen, also prüft der Server das signierte Token von Access gegen die veröffentlichten Schlüssel des Teams, wie Census.
  3. Dann ein eigenes CookieEine gültige Anmeldung bekommt ein von Atrium signiertes Sitzungs-Cookie, HttpOnly und Secure, für 30 Tage. Ein Gast bekommt nie ein Cookie.

Der Offline-Worker brauchte eine Ausnahme. Er beantwortet Seitenaufrufe aus dem Netz und fällt auf die zwischengespeicherte Seite zurück, wenn das scheitert. /login verlässt die Seite zur Anmeldeseite von Access auf einer anderen Origin, eine Weiterleitung, der der Abruf des Workers nicht folgen kann, also hätte er stattdessen die zwischengespeicherte Seite gezeigt. Der Worker lässt /login, /logout und Cloudflares eigene Pfade in Ruhe.

Eine Seite, die still hält

Eine Kachel öffnet die Seite ihrer App, unter /clinch, /cantina und so weiter: eine Seite statt eines Overlays, sie scrollt, lässt sich teilen und geht mit Zurück zurück wie jede andere. Wo der Browser View Transitions kann, wächst die Kachel in die Bühne der Seite und schrumpft beim Verlassen zurück an ihren Platz; anderswo wechselt die Seite einfach.

Alles, was sich bewegt, folgt dem Zeiger. Die Punkte hinter der Seite werden nur gezeichnet, während sich die Maus bewegt und sie sich wieder setzen, eine ruhende Seite zeichnet also kein einziges Frame, und Berührungen werden ignoriert, weil sie auf dem Handy Scrollen bedeuten. Kacheln bewegen sich nur unter dem Zeiger: Clinchs Klammern schließen sich, Cantinas Teller dreht sich, Fermatas Wellenform spielt, und Planums Grundriss kippt in 3D, die Wände als drei Ebenen, 10, 20 und 30 Pixel entlang der Z-Achse angehoben. Während die Seite scrollt, pausiert Hover, damit Kacheln, die unter einem ruhenden Zeiger durchgleiten, nicht nacheinander aufleuchten.