Hasenradar ist eine Webanwendung zur Überwachung und Diagnose von HASE-Öfen. Servicetechniker sehen darin Telemetrie, Diagramme und Fehlercodes, und für alles, was sich wiederholt, hat die Anwendung ein Terminal mit einer eigenen Skriptsprache: .hase. Hier beschreibe ich, warum sie nötig wurde, was in der ersten Version schiefging und wie die zweite aufgebaut ist.

Der .hase-Editor mit Syntaxhervorhebung und Codeprüfung
Der .hase-Editor. Die Screenshots entstanden mit den eingebauten Simulatoren, ohne Kundendaten.

Warum eine eigene Sprache

Viele Aufgaben eines Technikers bestehen aus denselben Schritten: sich mit einem Ofen verbinden, einen Parameter lesen, auf die richtige Temperatur warten, einen Bericht über mehrere Geräte erstellen, einen Eintrag in der Datenbank korrigieren. Von Hand dauert das lange, und bei der zehnten Wiederholung passiert leicht ein Fehler.

JavaScript kam dafür nicht in Frage. Code, der direkt in der Anwendung läuft, hätte Zugriff auf alles: die Sitzung des Nutzers, die Datenbank, die Daten anderer Geräte. Gebraucht wurde eine Sprache, die nur erlaubte Aktionen kennt und alles, was Daten verändert, vor dem Start sichtbar macht.

Die erste Version: Terminalbefehle

Die erste Version ist aus den Terminalbefehlen entstanden. Werte wurden als Text gespeichert, Variablen mit $ geschrieben, und zum Rechnen gab es einen eigenen Befehl calc. Dieses Beispiel aus der Dokumentation liest die Temperatur und schreibt, wenn sie über einem Grenzwert liegt, einen neuen Parameterwert:

get T as temp
if $temp > 400 {
  set PL 80
}

Für kurze Aktionen reichte das. Aber die Skripte wurden länger, und mit ihnen wuchs die Zahl der Fehler, die niemand sah.

Was schiefging

Im August 2026 habe ich die erste Version untersucht und Fehler gefunden, die eines gemeinsam hatten: Die Sprache lieferte falsche Ergebnisse, ohne es zu melden.

  • Terminal, Editor und die Codeprüfung im Editor lasen ein Skript jeweils auf ihre eigene Weise. Sie waren sich uneinig, wo ein String endet und was als Zahl gilt, und jede Abweichung ergab ein falsches Ergebnis ohne Fehlermeldung.
  • Ein einzelnes = in einer Bedingung, etwa if $a = 5 { }, schickte den Parser in eine Endlosschleife, und der Browser-Tab hing.
  • In JavaScript ergibt Number('') den Wert 0. Deshalb war bei einem Tippfehler im Variablennamen die Bedingung if $typo < 100 { set ... } wahr, und der Schreibbefehl ging an den Ofen.
  • Anführungszeichen wurden entfernt, bevor der Wert erkannt war. Deshalb verglich if $state == "connected" nicht mit dem String, sondern mit dem Verbindungsstatus.
  • Der Bestätigungsdialog für gefährliche Befehle prüfte nur die oberste Ebene eines Skripts, und ein Löschbefehl innerhalb eines if lief ohne Bestätigung durch.
  • Lange Controller-IDs liefen durch Number() und kamen als 1.0000283e+21 in die Datenbank zurück.
  • Nach einem einzigen Klick auf Stop brachen alle folgenden Schleifen bis zum Neuladen der Seite sofort ab. Ein Skript endete immer mit „Script completed“, auch wenn es in der dritten Zeile gescheitert war. Und set meldete Erfolg, sobald der Schreibbefehl in der Warteschlange stand, nicht erst, wenn die Datenbank ihn tatsächlich gespeichert hatte.

Die Korrektur begann beim Wichtigsten: Die Sprache bekam einen einzigen Parser, den Terminal, Editor und Prüfung gemeinsam nutzen, und Tests dafür. Syntaxfehler benennen jetzt das Problem und zeigen Zeile und Spalte. Ein Vergleich mit einer unbekannten Variable ist ein Fehler, ein Schreibbefehl wartet auf die echte Antwort der Datenbank, und Stop entfernt die Befehle aus der Warteschlange, die noch nicht beim Ofen angekommen sind.

Die zweite Version

Ende September kam die zweite Version der Sprache. Ein Skript wählt sie mit der Zeile #version 2, ältere Skripte ohne diese Zeile laufen wie bisher.

#version 2
fn classify(t, limit) {
  if t == null { return "unknown" }
  if t >= limit { return "warm" }
  return "cool"
}
let readings = [80, 120, null]
for value in readings {
  print(value, classify(value, 100))
}

Die Syntax kennt man aus JavaScript und Python: let, fn, if, for, while, try und catch. Das Verhalten unterscheidet sich aber überall dort, wo man in JavaScript leicht unbemerkt danebenliegt:

  • Vergleiche wandeln keine Typen um: 42 == "42" ist falsch. Ein fehlender Wert ist null, nicht 0, und der Zugriff auf eine nicht deklarierte Variable ist ein Fehler.
  • Controller-IDs, Seriennummern und Codes mit führenden Nullen werden als Strings geschrieben. Ganze Zahlen, die sich nicht exakt speichern lassen, lehnt die Sprache ab.
  • let copy = original kopiert die Daten, eine Änderung an der Kopie lässt das Original unverändert.
  • Ein Skript hat keinen Zugriff auf Browserobjekte, Netzwerk, Dateien, Zugangsdaten und eval. Aufrufen kann es nur Funktionen aus einer eingebauten Liste: Text, Listen, JSON und Statistik sowie Funktionen der Anwendung wie device.snapshot() und device.wait().

Wie ein Skript ausgeführt wird

Ein Skript wird nie in JavaScript umgewandelt. Der Text wird in Tokens zerlegt, daraus entsteht ein Baum, und ein Interpreter geht diesen Baum Knoten für Knoten durch. Parser und Interpreter der zweiten Version umfassen rund tausend Zeilen TypeScript.

Dieser Ansatz bietet eine Kontrolle, die es mit eval nicht gäbe:

  • Grenzen. Höchstens 100.000 Ausführungsschritte, eine Aufruftiefe von 64 und 10.000 Elemente in einer Sammlung. Eine Endlosschleife endet mit einem Fehler statt mit einem hängenden Tab.
  • Anhalten. Der Interpreter gibt regelmäßig die Kontrolle an den Browser zurück, deshalb greift Stop sofort. Befehle, die noch nicht an den Ofen gesendet wurden, werden aus der Warteschlange entfernt. Ein Schreibbefehl, der schon unterwegs ist, lässt sich nicht zurücknehmen, und die Dokumentation sagt das offen.
  • Prüfung vor dem Start. Vor der Ausführung sammelt der Interpreter alle Aufrufe aus dem Baum. Schreibvorgänge auf Geräte und in die Datenbank werden zur Bestätigung angezeigt, auch wenn sie in einem if-Zweig stehen, der vielleicht nie ausgeführt wird.
  • Verständliche Fehler. Ein Parserfehler verhindert den Start, ein Laufzeitfehler hält das Skript in der Zeile an, in der er auftritt. In beiden Fällen werden Zeile und Spalte angegeben.
Das Terminal mit der Ausgabe eines .hase-Skripts
Terminalbefehle und Skriptausgabe

Wofür es genutzt wird

Ein einfaches Beispiel ist ein Bericht über mehrere Öfen. Lässt sich ein Ofen nicht lesen, landet der Fehler in der Ausgabe, und die anderen werden trotzdem abgefragt:

#version 2
let ids = ["controller-A", "controller-B"]
let rows = [["ID", "T"]]
for id in ids {
  try {
    let data = device.snapshot(id)
    if data != null && type(data.T) == "number" {
      rows = push(rows, [id, data.T])
    }
  } catch error {
    print(id, error)
  }
}
print(csv.stringify(rows))

Die größten Skripte reparieren die Brennhistorie aller Öfen auf einmal. Sie haben mehrere hundert Zeilen, und das Skript zur Bereinigung der Historie beginnt mit einem Vorschaumodus und der Prüfung, ob die richtige Datenbank ausgewählt ist:

#version 2
const DRY_RUN = true    # erst nur ansehen, nichts ändern
const BATCH_SIZE = 25
const EXPECTED_DATABASE = "…"
assert(firebase.url() == EXPECTED_DATABASE, "STOPP: Falsche Datenbank ausgewählt!")

Weil es viele Öfen gibt und das Schrittlimit pro Lauf gilt, laufen solche Prüfungen in Durchgängen. Das Skript bearbeitet 25 Geräte, lädt das Ergebnis als JSON herunter und plant mit script.nextBatch() den nächsten Durchgang. Jeder Durchgang beginnt neu mit frischen Variablen, und von einem zum nächsten wird nur der Zustand weitergegeben, den das Skript ausdrücklich übergibt.

Fenster aus Skripten

Ein Skript kann ein echtes Hasenradar-Fenster öffnen. Dieses Widget zeigt die aktuellen Fehlercodes des verbundenen Ofens und aktualisiert sich selbst, ohne Schleife und Timer:

#version 2
#name Fehler
ui.widget({
  title: "Fehler",
  width: 200,
  height: 96,
  items: [{type: "errors"}]
})

Schaltflächen in diesen Fenstern brauchen keine Callbacks: ui.wait() wartet auf einen Klick und gibt ihn als gewöhnlichen Wert zurück, und was danach passiert, entscheidet das Skript in einer gewöhnlichen Schleife. Ein Skript lässt sich als eigener Terminalbefehl speichern und beim Öffnen der Anwendung starten. Wer keinen Code schreiben möchte, nutzt den visuellen UI-Editor, der den Code selbst erzeugt.

Der visuelle UI-Editor mit Vorschau
Der visuelle UI-Editor: eine Verbrennungsansicht mit Live-Daten des Demo-Ofens

Editor und Assistent

Skripte entstehen in Monaco, dem Editor, auf dem auch VS Code aufbaut: Hervorhebung, Autovervollständigung, Hinweise beim Überfahren mit der Maus und Fehler direkt im Text. Ein Skript lässt sich im eigenen Browser oder in der gemeinsamen Bibliothek des Teams speichern.

Mit der zweiten Version bekam der Editor einen Assistenten auf Basis von Gemini. Er schreibt, ändert und erklärt Skripte nach einer Beschreibung in normaler Sprache. Vor dem Senden zeigt er die vollständige Anfrage und blendet bekannte sensible Daten aus, und den vorgeschlagenen Code prüft er auf Syntaxfehler und unbekannte Funktionen. Im Oktober wurde der Assistent abgeschaltet, um die Kosten für die Generierung zu stoppen.

Was ich daraus mitnehme

  • Eine Sprache braucht einen einzigen Parser. Sobald Terminal und Editor Code unterschiedlich verstehen, werden Fehler unsichtbar.
  • Ein Fehler muss laut sein: mit Meldung, Zeile und Spalte. Ein Skript, das still in den else-Zweig läuft, ist gefährlicher als eines, das abbricht.
  • Alles, was Daten oder Geräte verändert, muss vor dem Start sichtbar sein.
  • Eine neue Version der Sprache darf alte Skripte nicht kaputt machen. Mit der Zeile #version 2 ließ sich das Verhalten ändern, ohne anzufassen, was schon funktionierte.