Alain Lilic

LiveRestaurant und HotelBottrop

Haus Wessels / Hotel Nike: Website, Betriebs-Cockpit und Betriebsfunk

Startseite von Haus Wessels / Hotel Nike in Bottrop am Desktop: Foto einer Holztür in einem Ziegelbogen, Überschrift 'Ein Haus, zwei Welten: Steaks vom offenen Feuer & Zimmer mit Frühstück', Navigation zu Restaurant, Hotel, Speisekarte, Räume, Feiern und Reservieren.
Startseite von Haus Wessels / Hotel Nike auf dem Smartphone: Bild der Holztür, Überschrift, Telefon-Schaltfläche und Menü-Schaltfläche.

Problem

Haus Wessels ist ein Familienbetrieb mit Steakhaus-Restaurant und Hotel in Bottrop. Reservierungen kommen per Telefon, Mail, Website und auf handgeschriebenen Zetteln, Lieferantenrechnungen auf Papier, Gastmails in ein gemeinsames Postfach. Die alte Website war über zehn Jahre alt. Gebraucht wurden eine neue Website und ein Werkzeug, das im laufenden Service vom Handy aus funktioniert.

Meine Rolle

Ich verantworte das Projekt allein: Anforderungen, Datenmodell, Architektur, Sicherheitsmodell, Prompts, Tests und Deploy. Ich schneide die Bauabschnitte, lasse jeden Abschnitt gegenprüfen, teste gegen eine Kopie der Produktionsdaten und entscheide, was live geht.

Vorgehen

Die Website baue ich mit Next.js 16 und Payload 3.88 auf SQLite. Speisekarte mit Pflichtfeld für Allergene, Zimmer, Räume mit Tischplänen und Anfragen pflegt der Betrieb selbst im CMS. Das Next.js/Payload-System mit Backend, Cockpit und API läuft per Docker hinter Caddy auf einem VPS. Öffentlich ausgeliefert wird ein geprüftes statisches Abbild auf dem bestehenden Hosting des Hauses. Veröffentlicht wird per git push: ein Hook baut, prüft das Abbild und überträgt nur geänderte Dateien.

Für den Alltag gibt es das Cockpit unter /verwaltung und einen Telegram-Bot auf meiner Sprach- und Chat-Agenten-Plattform. Im Bot fragt das Team in normaler Sprache nach Reservierungen, schickt Fotos von Zetteln und Rechnungen oder lässt Antworten auf Gastmails entwerfen. Ein Vision-Modell liest die Fotos, ein Sprachmodell formuliert. Eine einzige Lesefunktion speist Cockpit, Zähler und Telegram, dazu kommt ein Tagesbericht um 11 Uhr. Geschrieben wird nur über die signierte /api/v1 des Hauses, und erst nach einem Tipp auf eine Karte.

Architektur

  1. Eingang

    Website-Formular, Mail im Postfach des Hauses, Foto, Text oder Sprachnachricht im Telegram.

  2. Bot auf meiner Agenten-Plattform (Python)

    Sprachmodell mit Lese-Werkzeugen, Vision-Auslese für Belege und Zettel, Mail-Wächter, der das Postfach nur liest.

  3. Karte im Telegram

    Entwurf mit genauem Inhalt und Knöpfen. Ein Mensch tippt auf Eintragen oder Senden.

  4. Signierte /api/v1

    29 Routen. API-Schlüssel mit Scope plus HMAC über Zeit, Methode, Pfad und rohen Rumpf, Idempotenz-Schlüssel, Limit je Client.

  5. Payload und Cockpit

    Reservierungen, Belege, Post-Journal und Protokoll liegen beim Haus. Cockpit mit Heute, Anfragen, Post, Belegen, Fristen.

  6. Betrieb

    Docker Compose hinter Caddy. Mail-Anhänge nur über einen Entschärfer-Container ohne Netz. Öffentliches statisches Abbild per git push.

Entscheidung

Das Modell liest frei, schreibt aber nur Entwürfe

Der Bot darf Reservierungsbuch, Post, Belege und Fristen frei abfragen. Alles, was schreibt, wird eine Karte im Telegram mit dem genauen Inhalt. Erst der Tipp auf Eintragen oder Senden löst den signierten Aufruf aus, mit Idempotenz-Schlüssel gegen doppelte Tipps. Formulieren tut das Modell, entscheiden tut der Mensch. Dass der Tipp allein nicht reicht, habe ich gelernt, als einmal ein Entwurf ungeprüft hinausging. Seitdem trägt der Senden-Knopf einen Fingerabdruck des gezeigten Textes, und Bot und Hotel prüfen jede Gastmail vor dem Versand auf Anweisungen an die KI, Anrede und Gruß. Die einzige bewusste Ausnahme vom Kartenprinzip ist ein kurzer Vermerk an einem Beleg.

Code-Auszug

core/betriebsfunk.py (gekürzt)Seitlich scrollenpython
async def _werkzeug(agent: str, profil: dict, chat_id: str, name: str, args: dict,
                    notizen: list[str], *, wirt: str = "", ...) -> dict:
    """Einen Werkzeugaufruf ausführen. Lesen geht direkt ans Buch; alles, was
    schreibt, wird eine Karte (und eine Notiz fürs Gedächtnis), nur ein
    Vermerk am Beleg wirkt sofort. ..."""
    # ...
    if name == "entwurf_anlegen":
        absicht = {k: args.get(k) for k in _ENTWURF_FELDER}
        fehlt = _entwurf_fehlt(absicht)
        if fehlt:
            return {"fehlt": fehlt, "hinweis": "Keine Karte gesendet, erst nachfragen."}
        daten = _entwurf_daten(absicht)
        entwurf_id = await _karte_senden(agent, profil, chat_id, daten)
        # ...
        return {"entwurf_id": entwurf_id, "karte": "gesendet"}

async def _eintragen(agent: str, profil: dict, ereignis, entwurf_id: int,
                     *, trotzdem: bool) -> None:
    # ...
    idempotenz = f"telegram:{entwurf_id}" + (":tz" if trotzdem else "")
    try:
        antwort = await [...].reservierung_melden(
            profil, _nutzlast(daten, entwurf_id, trotzdem=trotzdem),
            idempotenz=idempotenz)
    # ...
Lese-Werkzeuge gehen direkt an das Buch des Hauses. Ein Schreibwunsch des Modells wird nur eine Karte. Erst der Tipp auf Eintragen ruft die signierte API, mit Idempotenz-Schlüssel gegen doppelte Tipps. Ein Modulname ist ausgelassen.

Ergebnisse

  • 29Routen unter /api/v1, jede mit API-Schlüssel, Scope und HMAC-Signatur
  • 9Scopes, getrennt nach Bereich und nach Lesen oder Schreiben
  • 5Karten höchstens je Block auf der Heute-Seite, vorher 67 Karten auf 15 Bildschirmen
  • 10/10richtige Bezüge im Nachtest mit echtem Modell nach der Prompt-Härtung vom 13.10.2026
17
Prüfstände im Hotel-Repo, dazu ein signierter Smoke-Test gegen die Produktion

Was ich daraus gelernt habe

Die Prüfung vor dem Versand von Gastmails hätte ich von Anfang an bauen sollen, nicht erst nach einem Fehler. Die erste Foto-Auslese hatte keine Messung und war für echte Zettel kaum brauchbar: Das Modell kannte das heutige Datum nicht, und mehrere Einträge auf einem Foto brachen das Parsen. Heute baue ich das Eval je Kategorie zuerst, mit der Regel: richtig oder als unsicher markiert, nie still falsch. Und Rechnungen hängen bisher nur über Notiztext an Aufenthalten. Ein festes Feld im Datenmodell würde ich von Anfang an anlegen.

Screenshots

Restaurantseite von Haus Wessels: Überschrift 'Steakhaus-Restaurant Haus Wessels', kurzer Text zur Küche, Schaltflächen 'Speisekarte ansehen' und 'Tisch reservieren', daneben ein Foto der Hausfassade.
hotel-nike-haus-wessels.de/restaurant