Apps
Eigene App bauen
Die App ist deine Webseite: im Look von Samy, mit samy.js und Anmeldung per Session-Token.
Eine App ist eine Webseite, die du selbst baust. Samy zeigt sie als Kachel im Tab Apps und öffnet sie unter dem Kopf der App, so breit wie der Bildschirm. Du hast alle Freiheit von HTML, und mit samy.css sieht deine Seite trotzdem aus wie Samy.
Wetter
Konto
Kanal oder App
| Kanal | App | |
|---|---|---|
| So geht es | Dein Tool schickt Nachrichten, Samy zeigt sie untereinander. | Die ganze App ist deine Webseite. |
| Du brauchst | nur HTTP-Aufrufe | einen Server, der die Seite über https:// ausliefert |
| Gut für | Meldungen: ein Server fällt aus, ein Deploy schlägt fehl, eine Bestellung kommt rein | Arbeit mit Listen und Daten: Rechnungen freigeben, Bestellungen ansehen, Tickets beantworten |
| Aussehen | immer wie Samy | mit samy.css wie Samy, sonst frei |
| Bedienung kommt an als | Webhook-Ereignis | normale Anfrage deiner Seite an deinen Server |
Fang mit einem Kanal an, wenn dein Tool sich vor allem melden soll. Auch dort gehen Knöpfe, Schalter, Diagramme und Tabellen, als Elemente in Nachrichten. Eine App lohnt sich, wenn du immer wieder etwas nachsehen oder abarbeiten willst.
Ideen, was eine App sein kann:
- Rechnungen freigeben: Jede offene Rechnung als Karte. Wisch nach rechts gibt sie frei, nach links lehnt sie ab.
- Server und Deploys: Status deiner Dienste, letzte Deploys, Knöpfe für Neustart und Rollback.
- Freigaben: Pull Requests oder Spesen ansehen und mit einem Tipp freigeben.
- Support-Postfach: Neue Anfragen lesen und kurz antworten.
- Bestellungen: Die Bestellungen deines kleinen Shops von heute, offen und versendet.
Wer Shopify kennt: Es ist dasselbe Prinzip wie bei eingebetteten Shopify-Apps mit App Bridge und Session-Token.
Du brauchst: einen Server mit https-Adresse, der deine Seite ausliefert.
1. Die Seite bauen
Eine normale HTML-Seite. Zwei Dateien von Samy machen sie zur App: samy.css für das Aussehen und samy.js für die Verbindung zur App.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://packlets.app/embed/samy.css">
<script src="https://packlets.app/embed/samy.js"></script>
</head>
<body>
<main class="samy">
<div class="samy-row samy-spread">
<div>
<p class="samy-label" id="firma">Rechnung</p>
<p class="samy-big" id="betrag">–</p>
<p class="samy-sub" id="info"></p>
</div>
<span class="samy-chip" id="offen"></span>
</div>
<div class="samy-row">
<button class="samy-button is-secondary samy-grow" onclick="entscheide('ablehnen')">Ablehnen</button>
<button class="samy-button samy-grow" onclick="entscheide('freigeben')">Freigeben</button>
</div>
</main>
<script>
let rechnung;
async function naechste() {
const res = await samy.fetch("/api/rechnungen/naechste");
rechnung = await res.json();
document.getElementById("firma").textContent = rechnung.firma;
document.getElementById("betrag").textContent = rechnung.betrag;
document.getElementById("info").textContent = `Fällig am ${rechnung.faellig} · ${rechnung.nummer}`;
document.getElementById("offen").textContent = `${rechnung.offen} offen`;
}
async function entscheide(aktion) {
const res = await samy.fetch(`/api/rechnungen/${rechnung.id}/${aktion}`, { method: "POST" });
if (!res.ok) return samy.toast("Das hat nicht geklappt");
samy.haptic("success");
naechste();
}
naechste();
</script>
</body>
</html>Die Klassen wie samy-row und samy-button kommen aus samy.css. Alle stehen in der Referenz.
2. Die Seite zur App machen
Leg in Samy ein Packlet an und wähle Eigene App bauen. Dann schickt dein Tool einmal die Adresse deiner Seite:
curl -X PUT https://api.packlets.app/v1/ui \
-H "Authorization: Bearer $SAMY_KEY" \
-H "Content-Type: application/json" \
-d '{"page": "https://dein-server.de/rechnungen"}'Antwort: {"ok": true, "page": "https://dein-server.de/rechnungen"}. Samy zeigt Verbunden!, und die Kachel steht im Tab Apps.
- Nur
https://. Im Body steht nurpage. - Beim ersten Öffnen zeigt Samy die Adresse deiner Seite und fragt nach, bevor es sie lädt. Wechselst du später auf eine andere Adresse (anderer Host oder Port), fragt Samy erneut.
- Eine andere Adresse setzt du mit einem neuen
PUT. - Einmal reicht. Was die Seite zeigt, bestimmt danach dein Server.
3. Mit der App sprechen: samy.js
Über das Objekt samy nutzt deine Seite, was nur die App kann:
| Aufruf | Was passiert |
|---|---|
samy.toast("Gespeichert") | Kurze Meldung in der App. |
samy.haptic("success") | Vibration. |
samy.open("https://…") | Öffnet eine Adresse im Browser. |
await samy.fetch(url, init) | Wie fetch(), aber mit Anmeldung. Siehe nächster Schritt. |
samy.ready() | Seite ist fertig, Samy blendet den Platzhalter aus (mit data-ready="manual"). |
samy.storage.set("ort", "Berlin") | Speichert Text auf dem Gerät, getrennt nach Konto. |
Samy hält geöffnete Seiten im Speicher. Kommt jemand zurück, ist deine Seite sofort da, ohne neu zu laden, und bekommt das Ereignis samy:activated. Samys Farben stehen als CSS-Variablen bereit (--samy-bg, --samy-text, --samy-cta …).
Im normalen Browser gibt es die App nicht, und die Aufrufe schlagen fehl. Frag deshalb vorher samy.inSamy ab, wenn deine Seite auch dort laufen soll:
<script>
if (!samy.inSamy) document.body.textContent = "Öffne diese Seite in Samy.";
</script>Alle Aufrufe stehen in der Referenz.
4. Wissen, wer die Seite öffnet
Deine Seite ist öffentlich erreichbar. Zeigt sie private Daten, muss dein Server wissen, dass die Anfrage wirklich aus deiner App in Samy kommt. Ein eigenes Login brauchst du dafür nicht: Samy weiß schon, wer angemeldet ist, und bestätigt es deinem Server mit einem Session-Token.
- Deine Seite ruft
samy.fetch()auf. Das holt bei Samy ein frisches Token und schickt es alsAuthorization: Bearer …an deinen Server. - Dein Server prüft das Token. Stimmt es, weiß er, wer die Seite öffnet.
Das Token ist ein JWT, 60 Sekunden gültig und signiert mit einem Geheimnis, das nur du und Samy kennen: dem SHA-256-Hash deines Schlüssels. Geprüft wird auf deinem Server:
import hashlib, os
import jwt # pip install pyjwt
SECRET = hashlib.sha256(os.environ["SAMY_KEY"].encode()).hexdigest()
PACKLET_ID = os.environ["SAMY_PACKLET_ID"]
def samy_user(request):
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
claims = jwt.decode(token, SECRET, algorithms=["HS256"], audience=PACKLET_ID)
return {"id": claims["sub"], "email": claims.get("email"), "name": claims.get("name")}Schlägt die Prüfung fehl, werfen beide Funktionen einen Fehler. Antworte dann mit 401.
SAMY_PACKLET_IDist die ID deines Packlets. Du bekommst sie in der Seite mit(await samy.info()).packlet_id, und sie steht in jedem Webhook-Ereignis alspacklet_id.- Kein Cookie, keine Sitzung.
samy.fetch()holt für jede Anfrage ein frisches Token. - Ohne JWT-Bibliothek kann dein Server das Token auch von Samy prüfen lassen.
Achtung: Der Schlüssel gehört auf deinen Server, nie in die Seite. Was im HTML oder JavaScript steht, kann jeder lesen.
5. iframes und fremde Inhalte
Deine Seite selbst ist kein iframe: Samy zeigt sie in einem eigenen Browser in der App. Zwei Dinge solltest du trotzdem regeln.
Fremde Inhalte in deiner Seite. Bettest du ein iframe von einer anderen Adresse ein (Video, Karte, Widget), kann es samy nicht benutzen. Samy gibt der Seite beim Laden einen geheimen Brücken-Schlüssel, den nur deine Seite sieht, und ignoriert jeden Aufruf ohne ihn. Was das iframe sonst tut, entscheidest du:
<iframe src="https://www.youtube-nocookie.com/embed/…"
sandbox="allow-scripts allow-same-origin"
referrerpolicy="no-referrer"></iframe>sandboximmer setzen und nur freigeben, was das iframe braucht. Ohneallow-top-navigationkann es deine Seite nicht wegschicken, ohneallow-popupskeine Fenster öffnen.- Fremde Skripte (Werbung, Tracking) direkt in deiner Seite laufen mit allen Rechten deiner Seite, auch mit
samy.fetch(). Bind nur Skripte ein, denen du vertraust. - Inhalte von Nutzern nie als HTML einsetzen (
innerHTML), sondern als Text (textContent).
Deine Seite in fremden Seiten. Deine Adresse ist öffentlich. Eine andere Webseite könnte sie in ein iframe laden und Nutzer zu Klicks verleiten. Ein Token bekommt sie dort nicht, weil es Samy dort nicht gibt. Verbiete das Einbetten trotzdem mit einem Header deines Servers:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYSamy stört das nicht, denn die App bettet deine Seite nicht in ein iframe ein.
Damit es sich wie eine App anfühlt
- Weißer Hintergrund, kein eigener Rahmen und kein Schatten außen herum.
- Kein eigener Kopf. Name und Avatar zeigt Samy schon. Was wichtig ist, gehört nach oben.
- Tippflächen mindestens 44 Pixel hoch, Text mindestens 12 Pixel.
- Kein Hover, keine Pop-ups, kein
alert(). Für Meldungen gibt essamy.toast(). - Links auf deiner eigenen Adresse bleiben in der App. Links zu anderen Adressen öffnen sich im Browser.
- Testen kannst du im Browser in Handy-Breite, zum Beispiel mit 375 Pixeln in den Entwicklerwerkzeugen.
Benachrichtigen
Eine App kann auch Nachrichten schicken, mit demselben POST /v1/push wie ein Kanal. Die Nachricht kommt als Push, und die Kachel zeigt einen Zähler. So meldet sich zum Beispiel die Rechnungs-App, wenn neue Rechnungen warten. In der App selbst siehst du nur deine Seite, keinen Verlauf.
Ausprobieren
Die beiden Seiten oben sind echte Apps:
- Wetter zeigt, wie frei du gestalten kannst. Jedes neue Konto hat sie in Samy unter Apps.
- Konto zeigt das Session-Token: Die Seite holt bei jeder Anfrage ein frisches Token, ihr Server prüft es und zeigt deinen Namen. Im Browser zeigt dieselbe Seite nur einen Hinweis, denn dort gibt es kein Token.
Checkliste
- Adresse der Seite mit
PUT /v1/uiund{"page": "https://…"}geschickt. <meta name="viewport" content="width=device-width, initial-scale=1">gesetzt.samy.cssundsamy.jseingebunden.- Anfragen an deinen Server laufen über
samy.fetch(). - Dein Server prüft bei jeder Anfrage Signatur, Ablauf und
auddes Tokens. - Der Schlüssel steht nur auf dem Server.
- Fremde iframes mit
sandbox, deine Seite mitframe-ancestors 'none'ausgeliefert.
Wie es weitergeht
- Alle Aufrufe von
samy.js, das Token undsamy.css: Referenz. - Feld, Antwort und Fehler von
PUT /v1/ui: PUT /v1/ui.