Diese Seite beschreibt, wie das Enerface-Dashboard gebaut ist, welche Funktionen es hat, wie es konfiguriert wird, wie das Docker-Image betrieben wird und wie man es lokal startet und ein erstes Konto anlegt. Sie ist öffentlich — sie enthält keine Geheimnisse.
1. Überblick
Enerface ist eine private, mandantenfähige Weboberfläche für Photovoltaikanlagen. Jedes Konto besitzt genau eine Anlage, identifiziert über die Basisnummer (z. B. 58). Es gibt keine öffentliche Registrierung: Konten legt nur ein Administrator unter /administration an.
Der Browser spricht nie direkt mit der Datenschicht. Ein einziges Go-Binary (cmd/enerface) liefert die statische Oberfläche, die JSON-API, den SQLite-Cache und die Hintergrundbefüllung. Die Oberfläche ist HTML/CSS/JS plus lokal eingebettetes Apache ECharts — kein CDN, kein Framework, kein Build-Schritt für das Frontend.
Die Zeitzone ist global (Europe/Zurich), die Währung fest CHF. Beides ist Absicht: die Oberfläche ist schweizerisch (Ortschaftssuche über GeoAdmin, Kantonszugehörigkeit), und eine zweite Zeitzone würde die Tagesaggregate im Cache nach Zeitzone schlüsseln müssen.
2. Warum es ein Backend gibt
Die Zeitreihen leben ausschliesslich auf der Datenschicht 1.grafana.enerface.app (Grafana vor InfluxDB, Bucket testing). Die Portal-API api.enerface.app kennt nur Momentanwerte, keine Historie.
Gemessen 2026-08-11: die Datenschicht sendet keine CORS-Header. Ein Preflight OPTIONS /api/ds/query antwortet 404, normale Antworten tragen kein Access-Control-Allow-Origin. Ein Browser darf sie deshalb nicht direkt abfragen.
Das Backend spricht server-to-server mit Grafana und arbeitet als Cache-through-Speicher:
- Abgeschlossene Intervalle (deren Ende in der Vergangenheit liegt) werden dauerhaft in SQLite gehalten.
- Das laufende, noch offene Intervall wird bei jeder Anfrage frisch geholt und nie gespeichert.
- Aggregation geschieht in Flux (
aggregateWindow), nicht im Backend und nicht im Browser.
Der anonyme Zugriff auf die Datenschicht ist eine Fehlkonfiguration des Betreibers. Wenn er geschlossen wird, bleibt alles bereits Geholte nutzbar; nur neue Daten fehlen. Ein reiner Durchreich-Proxy ohne Cache würde in dem Moment wertlos.
3. Aufbau
Browser
/ Dashboard (ECharts)/administration Kontenverwaltung
Session-Cookies, kein JWT
Go-Prozess
cmd/enerface
eingebettetes UI, JSON-API, Prefetch-Schleife, argon2id
Datenschicht
Grafana 12 → InfluxDB
Flux, Topic enerbase/<base>/data
anonym, ohne CORS
SQLite
data/cache.sqlite (WAL)
Samples, Coverage, Konten, Sessions, Einstellungen, Logos
Open-Meteo
Zwei-Tage-Tagesprognose 08–19 Uhr, 15-Minuten-Speicher, Schlüssel Koordinaten
GeoAdmin
Schweizer Ortschaftssuche und Kanton zu einem Punkt (api3.geo.admin.ch)
Pakete
| Pfad | Aufgabe |
|---|---|
cmd/enerface | Flags, Listen, Prefetch, -healthcheck, -hash-password |
internal/config | TOML der Datenschicht, Admin-Geheimnisse aus der Umgebung, .env |
internal/server | HTTP, Sessions, Admin-API, statische Dateien, Sicherheitsheader |
internal/cache | SQLite: Zeitreihen nach Basisnummer, Konten, Coverage |
internal/energy | Anlage, Cache-through, Tiers, Kennzahlen, Live, Prefetch |
internal/upstream | Grafana/Flux-Client, Open-Meteo, GeoAdmin |
ui/ | Dashboard, Administration, diese Seite; go:embed ins Binary |
Die statischen Dateien liegen im Binary. Mit -static ui (so starten dev.sh / dev.ps1) werden HTML/JS/CSS von der Platte gelesen, damit Frontend-Änderungen keinen Rebuild brauchen.
Antworten ab 1000 Byte werden gzip-komprimiert, wenn der Client das anbietet. /api/* ist Cache-Control: no-store; Vendor-Dateien unter /static/vendor/ sind ein Jahr unveränderlich cachebar.
4. Konten und Anlagen
Die Anlage ist eine Eigenschaft des Kontos, nicht des Prozesses. Der Prozess hält keine globale Anlage mehr.
- Konto. E-Mail (nur Identifier, es gibt keine Mail-Infrastruktur), argon2id-Hash, Basisnummer, Flag «erster Tag unvollständig», Sperrflag. Lebt in SQLite.
- Administrator. Kein Konten-Datensatz. Zugangsdaten
ADMIN_USER/ADMIN_PASSWORD_HASHaus der Umgebung. Eigenes Cookieenerface_admin, Standardlebensdauer 8 Stunden. - Benutzer-Session. Cookie
enerface_session, Standard 30 Tage, anaccount_idgebunden. Passwort-Reset und Sperren verwerfen jede Session dieses Kontos. - Keine Impersonation. Die Admin-API ist
/api/admin/*. Ein Administrator handelt nie «als» Benutzer. - Kein hartes Löschen. Nur Sperren. Ein Konto besitzt Samples, Einstellungen und hochgeladene Logos.
Zwei Konten dürfen dieselbe Basisnummer teilen — dann teilen sie den Cache, was korrekt ist, weil es dieselbe physische Anlage ist. Der Cache ist nach Basisnummer geschlüsselt, nicht nach Konto: Samples von Basis A sind für Basis B unsichtbar.
5. Datenpfad
Rohdaten kommen als MQTT-Nachrichten, eine pro Minute, Measurement mqtt_consumer, Topic enerbase/<basis>/data. Energiekanäle sind Wh pro Minute: Summe über den Zeitraum ÷ 1000 = kWh. Die Skalierungsfaktoren der Enerface-Dashboards (0.012, 0.2) sind fensterabhängig und werden bewusst nicht übernommen.
Auflösungsstufen (Tiers)
| Tier | Fenster | Auto-Wahl | Limit (erzwungen) | Prefetch |
|---|---|---|---|---|
1mo | Kalendermonat, lokal | Spanne > 400 Tage | — | ja |
1d | Kalendertag | > 21 Tage | — | ja |
1h | Stunde | > 3 Tage | — | ja |
15m | 15 Minuten | > 36 Stunden | 180 Tage | ja |
1m | Minute | sonst | 14 Tage | nein, nur on demand |
auto wählt die Stufe nach der Länge des angefragten Zeitraums, damit ein Jahr nicht als Minutenwerte ankommt. Flux muss option location = timezone.location(name: "Europe/Zurich") setzen, sonst landen Tagesgrenzen auf UTC. In Flux ist 1m eine Minute und ein Monat 1mo.
Cache-through
Die Tabelle covered unterscheidet «keine Daten in diesem Fenster» von «noch nie geholt». Ohne sie würde eine Lücke bei jedem Reload erneut die Datenschicht belasten, und ein leeres Fenster würde fälschlich als «erledigt» gelten.
Minutenwerte werden in 10-Tage-Blöcken abgefragt. Die Grafana-Antwort kommt entweder breit (ein Frame, viele Spalten) oder als ein Frame pro Kanal; der Client versteht beides. Fehler der Datenschicht stehen oft im Body bei HTTP 200 — ausgewertet wird results.A.error.
Hintergrundbefüllung
Fünf Sekunden nach Start, danach alle -prefetch (Standard 1 Stunde), wärmt eine Goroutine die Tiers 1mo / 1d / 1h / 15m für jedes Konto, das in den letzten 7 Tagen angemeldet war. Zwischen den Konten liegen 2 Sekunden, damit ein Aufwachen nicht als Burst bei Grafana ankommt. Ein fehlgeschlagenes Konto bricht den Durchlauf nicht ab. /api/admin/health zeigt das Ergebnis des letzten Laufs.
Live-Werte
GET /api/live benutzt dieselbe Pipeline wie die Zeitreihe, mit den letzten zehn Minuten auf 1m. Wh/min × 60 ergibt kW. Älter als 10 Minuten setzt stale: true; das Frontend zeigt dann ein Banner. Es gibt keinen zweiten Upstream-Aufruf und keine Extra-Credentials.
6. Kanäle und Kennzahlen
Die Kanalbelegung ist die validierte Enerbase-Verdrahtung. Sie wird pro Konto gespeichert, damit eine Anlage später abweichen kann, ist aber heute nicht editierbar — ein anderes Messkonzept braucht Code.
| Kanal | Bedeutung | Roh | Aggregation | |
|---|---|---|---|---|
ch1 | PV-Produktion | Wh/min | Summe | |
ch25 | Gesamtverbrauch | Wh/min | Summe | |
ch26 | Eigenverbrauch | Wh/min | Summe | |
ch2 | Netzeinspeisung | Wh/min | Summe | |
ch3 | Netzbezug | Wh/min | Summe | |
ch10 | Batterieladung | Wh/min | Summe | |
ch9 | Batterieentladung | Wh/min | Summe | |
ch7 | Batterie-SOC | % | Mittel | |
ch8 | Batterieleistung | W | Mittel; positiv = laden | |
ch6 | Boiler-Einschaltdauer | 0/1 | Mittel = Duty Cycle |
Farben sind geprüft, nicht geraten: ein Kanal behält seinen Farbton in jedem Chart, Paare auf einem Chart sind für Farbfehlsichtigkeit in Hell und Dunkel validiert. Netzbezug ist Violett statt Magenta, weil Magenta neben dem Aqua des Eigenverbrauchs unter Deuteranopie zusammenfiel.
Kennzahlen über den gewählten Zeitraum:
- PV-Produktion, Eigenverbrauch, Autarkie (Eigenverbrauch / Gesamtverbrauch), Eigenverbrauchsquote (Eigenverbrauch / PV)
- Spezifischer Ertrag (kWh/kWp), sobald kWp gesetzt ist
- SOC-Min/Max der Intervallmittel — nicht die wahren Extremwerte der Minute
- Optional Geld: Ersparnis Eigenverbrauch, Einspeisevergütung, Netzbezugskosten — nur wenn der jeweilige Tarif gesetzt ist, sonst erscheint die Kachel gar nicht
Die Identität Verbrauch = Eigenverbrauch + Netzbezug hält bis auf Rundung der ganzzahligen Rohwerte (Grössenordnung 0.1 kWh/Tag). Das Gegenstück PV = Eigenverbrauch + Einspeisung gilt nicht: die Batterie puffert über Tagesgrenzen, plus Wandlungsverluste. Die Oberfläche behauptet diese Identität deshalb nicht.
Ein Konto kann den ersten Betriebstag als «unvollständig» markieren (Messung PV schon da, Verbrauchszähler noch nicht). Der Tag wird in Tabelle und Fussnote gekennzeichnet und verzerrt Summen und Quoten.
7. Funktionen
Dashboard (/)
- Anmeldung mit E-Mail und Passwort; Selbständerung des Passworts (mindestens 8 Zeichen, aktuelles Passwort nötig)
- Deutsch / Englisch, Hell / Dunkel (beides im
localStorage) - Live-Leiste: PV, Verbrauch, Netz, Batterie, SOC; Banner wenn älter als 10 Minuten
- Wetterleiste (Open-Meteo), sobald eine Ortschaft gesetzt ist; ein/aus, merkt sich den Zustand
- Wappen-Banner: Ortschaft + Kanton links, Anzeigename + Logo rechts — leer, solange nichts konfiguriert ist
- Zeitraum: Heute, Gestern, 7/30/90 Tage, Jahr, Alles, benutzerdefiniert; Vor/Zurück
- Auflösung automatisch oder erzwungen
- Charts: Produktion und Verbrauch; Wohin die Produktion geht; Wie der Verbrauch gedeckt wird; Batterieenergie; SOC; Batterieleistung; Boiler. Ein Chart kann den Viewport füllen. Nie zwei Y-Achsen auf einem Plot.
- Tabellenansicht als barrierefreies Gegenstück, CSV-Export
- Einstellungen: Ortschaft (GeoAdmin-Vorschläge), Logos, kWp, Batterie-kWh, Daten ab, Bezugspreis und Einspeisetarif in CHF/kWh
Administration (/administration)
- Eigene Anmeldung, eigener Cookie, eigener Fehlversuchszähler
- Allgemeine Informationen: Erreichbarkeit der Datenschicht, Datenbankgrösse, Kontenzahl, aktive Konten (7 Tage), letzter Prefetch, Cache je Tier
- Konto anlegen: E-Mail + Basisnummer. Passwort wird erzeugt (vier Vierergruppen, ohne leicht verwechselbare Zeichen) und einmal angezeigt.
data_startkommt automatisch aus der Flux-first()-Abfrage. - Konto bearbeiten: E-Mail, Basis (mit Warnung), unvollständiger erster Tag, alle Benutzer-Einstellungen inklusive Logos
- Passwort zurücksetzen (wieder einmalig, Sessions weg) und Konto sperren (Sessions weg)
Ohne ADMIN_USER / ADMIN_PASSWORD_HASH antwortet die Admin-Anmeldung 503. Ohne Konten kann sich niemand am Dashboard anmelden.
8. HTTP-Schnittstelle
Alles unter /api/ ausser /api/login, /api/admin/login und /api/healthz braucht eine Session. Kontobezogene Antworten stammen immer vom Aufrufer — nie von einem anderen Konto.
| Methode | Pfad | Zweck |
|---|---|---|
| POST | /api/login | {"email","password"} → Cookie enerface_session |
| POST | /api/logout | Session beenden |
| POST | /api/password | {"current","new"} Selbständerung |
| GET | /api/meta | Stammdaten, Kanäle, Cache-Stand der eigenen Basis |
| GET | /api/series | start/stop als lokale Tage YYYY-MM-DD, tier=auto|1mo|1d|1h|15m|1m |
| GET | /api/live | Neueste Minute |
| GET | /api/weather | Prognose der konfigurierten Ortschaft |
| GET/PUT | /api/settings | Eigene Dashboard-Einstellungen |
| GET/PUT/DELETE | /api/settings/{city,name}-logo | Hochgeladene Wappen/Logos |
| GET | /api/geo/cities, /kanton | Ortschafts- und Kantonssuche |
| GET | /api/healthz | Liveness, ohne Session, ohne Geheimnisse |
Die Admin-API sitzt hinter enerface_admin. Ein Dashboard-Cookie reicht für keinen dieser Pfade.
| Methode | Pfad | Zweck |
|---|---|---|
| POST | /api/admin/login | {"username","password"} |
| POST | /api/admin/logout | |
| GET | /api/admin/health | Datenschicht, DB-Zahlen, Prefetch |
| GET/POST | /api/admin/accounts | Liste / Anlegen (Passwort einmal im Body) |
| GET/PUT | /api/admin/accounts/{id} | Detail / E-Mail, Basis, Teiltag |
| PUT | /api/admin/accounts/{id}/settings | Benutzer-Einstellungen stellvertretend |
| GET/PUT/DELETE | /api/admin/accounts/{id}/{city,name}-logo | |
| POST | /api/admin/accounts/{id}/reset-password | Neues Passwort einmal; Sessions weg |
| POST | /api/admin/accounts/{id}/disabled | {"disabled":true}; Sessions weg |
Öffentliche HTML-Seiten ohne Session: /, /administration, /architecture.html. Die APIs dahinter sind getrennt geschützt.
9. Konfiguration
Drei Orte, sie überlappen sich nicht:
| Ort | Inhalt | Warum dort |
|---|---|---|
config.toml | Zeitzone, Grafana-URL, Datasource-UID, Bucket, HTTP-Timeout | Commitbar: nichts Geheimes, nichts Pro-Kunde |
Umgebung / .env | ADMIN_USER, ADMIN_PASSWORD_HASH | Geheim, und der Administrator hat keine Kontenzeile |
SQLite über /administration | Konten und alles Pro-Anlage: Basis, kWp, Batterie, Preise, Ortschaft, Logos, data_start | Laufzeit, genau der Sinn der Admin-Seite |
config.toml
timezone = "Europe/Zurich"
[grafana]
url = "https://1.grafana.enerface.app"
ds_uid = "P951FEA4DE68E13C5"
bucket = "testing"
http_timeout = "5m"
Vorlage: config.example.toml. Fehlt die Datei, kopieren dev.sh und dev.ps1 sie. Im Image liegt dieselbe Vorlage unter /etc/enerface.toml — abweichende Endpunkte per Bind-Mount überschreiben.
Umgebung
ADMIN_USER=admin
ADMIN_PASSWORD_HASH=argon2id$v=19$m=65536,t=3,p=4$...$...
Hash erzeugen:
go run ./cmd/enerface -hash-password
# Passwort tippen, dann Ctrl-D (Unix) bzw. Ctrl-Z Enter (Windows)
Unter Docker Compose jedes $ im Hash als $$ schreiben, sonst interpoliert Compose $v / $m. Bestehende Umgebungsvariablen gewinnen gegen .env (set-if-absent).
In der Entwicklung ist nichts davon nötig: dev.sh und dev.ps1 setzen admin / admin selbst und zeigen es beim Start an (§12).
Prozessflags
| Flag | Standard | Bedeutung |
|---|---|---|
-listen | 127.0.0.1:8099 | Bind-Adresse. Im Container :8099 |
-db | data/cache.sqlite | SQLite-Pfad. Im Container /data/cache.sqlite |
-config | config.toml | Upstream-TOML. Im Container /etc/enerface.toml |
-static | (leer = Embed) | UI von der Platte, für die Entwicklung |
-secure-cookie | false | Cookie-Flag Secure — sobald TLS davor sitzt, einschalten |
-session-ttl | 720h | Benutzer-Session |
-admin-session-ttl | 8h | Admin-Session (nur im Speicher; Neustart loggt den Admin aus) |
-prefetch | 1h | Hintergrundbefüllung; Werte unter einer Minute werden auf eine Minute angehoben |
-healthcheck | — | Fragt /api/healthz und beendet sich; Healthcheck des Images |
-hash-password | — | argon2id-Hash von stdin nach stdout |
data/cache.sqlite hält den Cache und die Konten. Löschen kostet nicht nur einen Refill — jedes Konto ist weg. Die Zeitreihen bauen sich neu; die Konten nicht.10. Docker-Image hosten
Was das Image ist
Mehrstufig: Build in golang:1.26-bookworm (CGO_ENABLED=0, Tests, -trimpath -ldflags="-s -w"), Laufzeit gcr.io/distroless/static-debian12. Distroless hat keine Shell — deshalb Exec-Form für ENTRYPOINT, CMD und HEALTHCHECK. Zeitzonendaten sind ins Binary kompiliert (time/tzdata). SQLite ist reines Go (modernc.org/sqlite), kein CGO, kein glibc-Bedarf zur Laufzeit.
- Benutzer
10001, unprivilegiert - Port
8099 - Volume
/data— der einzige Schreibpfad config.example.tomlliegt unter/etc/enerface.toml- Healthcheck alle 30 s:
/enerface -healthcheck -listen :8099
ENTRYPOINT ["/enerface"]
CMD ["-listen", ":8099", "-db", "/data/cache.sqlite", "-config", "/etc/enerface.toml"]
Image beziehen
Das veröffentlichte Image liegt in der Forgejo-Registry derselben Instanz, nicht auf Docker Hub:
source.suntsu.ch/suntsu/enerfacedemo
Der Workflow Publish Docker image läuft nur von Hand (Repository → Actions → Run workflow). Er taggt immer den kurzen Commit-SHA, auf Wunsch latest und ein frei wählbares Extra-Tag. Anmeldung mit dem Repository-Secret REGISTRY_TOKEN (Forgejo-Token mit write:package). Provenance-Attestations sind abgeschaltet, damit in der Paketansicht kein unknown/unknown erscheint.
docker login source.suntsu.ch
docker pull source.suntsu.ch/suntsu/enerfacedemo:latest
Lokal bauen
./dockerbuild.sh # taggt enerface:local
ENERFACE_IMAGE=enerface:dev ./dockerbuild.sh
Direkt starten
docker login source.suntsu.ch
docker run -d --name enerface \
--restart unless-stopped \
-p 8099:8099 \
-v enerface-data:/data \
-v "$PWD/config.toml:/etc/enerface.toml:ro" \
-e ADMIN_USER='admin' \
-e ADMIN_PASSWORD_HASH='argon2id$v=19$m=65536,t=3,p=4$...' \
source.suntsu.ch/suntsu/enerfacedemo:latest \
-listen :8099 -db /data/cache.sqlite -config /etc/enerface.toml -secure-cookie
-secure-cookie gehört dazu, sobald TLS (Reverse-Proxy) davor sitzt. Ohne Volume ist der Cache — und jedes Konto — nach einem Recreate weg.
Docker Compose / Podman
docker-compose.yml mappt den Host-Port 8081 auf Container-Port 8099, nimmt Admin-Daten aus .env, mountet ./config.toml und ./data:
# einmal, die Registry ist privat
docker login source.suntsu.ch
# oder: podman login source.suntsu.ch
mkdir -p data # bevor compose das Verzeichnis als root anlegt
cp config.example.toml config.toml
cp .env.example .env # Hash einsetzen; $ als $$ escapen
docker compose up -d
# veröffentlichtes Image statt lokalem Build:
# ENERFACE_IMAGE=source.suntsu.ch/suntsu/enerfacedemo:latest docker compose up -d
Das Image läuft als uid 10001 und kann ein von Ihnen besessenes Host-Verzeichnis nicht beschreiben. Compose setzt deshalb user: "${UID:-1000}:${GID:-1000}". Legen Sie ./data vorher an — sonst erzeugt Compose es als root.
Compose-Command (weicht vom Image-CMD ab): -listen=:8099 -db=/data/cache.sqlite -config=/etc/enerface.toml -secure-cookie=false. Hinter TLS diese letzte Zeile auf true stellen oder das Flag ohne Wert übergeben.
Reverse-Proxy und TLS
- TLS am Proxy terminieren (Caddy, nginx, Traefik) und auf
8099(bzw. den gemappten Host-Port) durchreichen. -secure-cookiesetzen, sonst schickt der Browser das Session-Cookie nicht über HTTPS.- Login-Sperre zählt nach
RemoteAddr, nicht nachX-Forwarded-For. Hinter einem Proxy sehen alle Versuche wie eine IP aus — die Sperre greift global, nicht pro Client. Das ist akzeptiert; ein vorgeschaltetes Fail2ban/Rate-Limit am Proxy bleibt sinnvoll. - Die App setzt
X-Frame-Options: DENY,Referrer-Policy: no-referrer,X-Robots-Tag: noindex, nofollowund eine restriktive CSP (default-src 'self', kein CDN).
Aktualisieren, Backup, Diagnose
docker compose pull
docker compose up -d
# Backup: Datei kopieren, während WAL läuft lieber den Container kurz stoppen
docker compose stop
cp data/cache.sqlite backup/cache-$(date +%F).sqlite
docker compose start
# Health
curl -fsS http://127.0.0.1:8081/api/healthz
docker inspect --format='{{.State.Health.Status}}' enerface
Ein Image-Update ersetzt nur das Binary. /data und die gemountete config.toml bleiben. Es gibt keinen Migrationspfad für vor-Mandanten-Datenbanken: eine alte sample-Tabelle ohne Spalte base lässt den Prozess mit einer klaren Fehlermeldung nicht starten — Datei löschen und Konten neu anlegen.
11. Sicherheit
- Passwörter: argon2id, m=65536, t=3, p=4. Unbekannte E-Mail und falsches Passwort sind ununterscheidbar (gleicher Aufwand über einen Dummy-Hash).
- Sperre: 5 Fehlversuche in 15 Minuten, pro IP und pro Benutzername, Admin getrennt. Zähler liegen im Speicher und fallen beim Neustart auf null.
- Cookies:
HttpOnly,SameSite=Lax,Path=/,Securenur mit Flag. - Admin-Sessions nur im Speicher; Benutzer-Sessions in SQLite, widerrufbar.
- CSP verbietet Inline-Scripts und fremde Origins. ECharts und Mermaid liegen unter
/static/vendor/. - Die Oberfläche ist privat:
noindex, nofollowin Meta und Header. - Admin-Aktionen gehen ins Prozess-Log (
slog) mit Konto, Aktion, Zeitpunkt — keine Audit-Tabelle.
12. Entwicklung: starten
Für die Entwicklung braucht es kein Docker — eine Go-Toolchain genügt. Die beiden Startskripte dev.sh (Linux, macOS) und dev.ps1 (Windows) tun dasselbe: fehlende Konfiguration anlegen, einen Administrator einrichten, den Prozess mit -static ui starten. Dadurch werden Änderungen an HTML, CSS und JS ohne Rebuild sichtbar — ein Reload im Browser reicht.
Voraussetzungen
- Go 1.26 oder neuer auf dem
PATH(go version). Mehr nicht: SQLite ist reines Go (modernc.org/sqlite), es braucht weder CGO noch einen C-Compiler. - Kein Frontend-Build. Kein Node, kein npm, kein Bundler; ECharts und Mermaid liegen fertig unter
ui/vendor/. - Unter Windows PowerShell 7 (
pwsh). - Ausgehender Zugriff auf
1.grafana.enerface.app, sonst bleiben die Charts leer. Anmeldung, Administration und Kontoverwaltung funktionieren auch ohne.
Linux und macOS
./dev.sh # http://127.0.0.1:8099, UI aus ./ui
PORT=9000 ./dev.sh # anderer Port
HOST=0.0.0.0 ./dev.sh # im LAN erreichbar, ohne TLS — mit Bedacht
Ist das Skript nicht ausführbar: chmod +x dev.sh, oder einmalig sh dev.sh. Beenden mit Ctrl-C.
Windows
.\dev.ps1 # http://127.0.0.1:8099, UI aus .\ui
$env:PORT = '9000'; .\dev.ps1 # anderer Port
$env:HOST = '0.0.0.0'; .\dev.ps1 # im LAN erreichbar, ohne TLS — mit Bedacht
Blockiert die Ausführungsrichtlinie das Skript, genügt ein einzelner Lauf ohne sie: pwsh -ExecutionPolicy Bypass -File .\dev.ps1. Beenden mit Ctrl-C. $env:PORT gilt für die ganze Shell-Sitzung — für einen einmaligen Port entweder eine neue Shell öffnen oder hinterher Remove-Item Env:PORT.
Dieselbe Aufgabe, beide Plattformen
| Aufgabe | Linux / macOS | Windows (PowerShell 7) |
|---|---|---|
| Starten | ./dev.sh | .\dev.ps1 |
| Anderer Port | PORT=9000 ./dev.sh | $env:PORT = '9000'; .\dev.ps1 |
| Im LAN | HOST=0.0.0.0 ./dev.sh | $env:HOST = '0.0.0.0'; .\dev.ps1 |
| Eigener Administrator | ADMIN_USER=ich ADMIN_PASSWORD_HASH='argon2id$…' ./dev.sh | $env:ADMIN_USER = 'ich'; $env:ADMIN_PASSWORD_HASH = 'argon2id$…'; .\dev.ps1 |
| Hash erzeugen | printf 'geheim' | go run ./cmd/enerface -hash-password | 'geheim' | go run ./cmd/enerface -hash-password |
| Tests | go test ./..., einzeln z. B. go test ./internal/server -run TestAdminLogin | |
| Binary bauen | go build ./cmd/enerface | |
| Beenden | Ctrl-C | |
Administrator-Zugang in der Entwicklung
Beide Skripte richten einen festen lokalen Administrator ein und geben ihn beim Start auf der Konsole aus — es muss nichts nachgeschlagen und nichts in eine Datei geschrieben werden:
==> hashing the development admin password
==> http://127.0.0.1:8099
==> administration: http://127.0.0.1:8099/administration
user: admin password: admin
| Wert | |
|---|---|
| Benutzername | admin |
| Passwort | admin |
| Adresse | http://127.0.0.1:8099/administration |
- Der Hash wird bei jedem Start neu erzeugt, über denselben
-hash-password-Pfad, den der Server zur Prüfung benutzt. argon2id ist gesalzen — ein fester Hash liesse sich nicht sinnvoll ins Skript schreiben, und ein veraltetes Format fiele erst bei der Anmeldung auf. - Gesetzt wird das in der Prozessumgebung, nicht in
.env.LoadDotEnvist set-if-absent, deshalb gewinnt die Umgebung; die.envbleibt unverändert. - Eigene Zugangsdaten gewinnen: sind
ADMIN_USER/ADMIN_PASSWORD_HASHvor dem Start gesetzt, übernimmt das Skript sie und zeigt kein Passwort an — es kennt nur den Hash. - Beim allerersten Start meldet das Log
no accounts yet. Das ist erwartet: der Administrator legt das erste Konto an (§13).
admin / admin ist ausschliesslich für die lokale Entwicklung. Im Betrieb kommen ADMIN_USER und ADMIN_PASSWORD_HASH aus der Umgebung des Hosts (§9) — die Startskripte werden dort nicht benutzt.Was die Skripte sonst noch tun
- Fehlt
config.toml, wirdconfig.example.tomlkopiert; fehlt.env, wird.env.examplekopiert. - Gestartet wird
go run ./cmd/enerface -listen <host>:<port> -static ui -config config.toml -db data/cache.sqlite. Ohne--vor den Flags: Go behandelt ein alleinstehendes--als Ende der Flag-Liste und würde alles danach stillschweigend verwerfen — inklusive-static ui. - Die Datenbank
data/cache.sqliteentsteht beim ersten Start und überlebt Neustarts. Sie enthält auch die Konten — löschen heisst neu anlegen.
13. Konto und Anlage anlegen
Konten entstehen nur in der Oberfläche unter /administration, nie über eine Registrierung und nie von Hand in der Datenbank. Der Ablauf ist in der Entwicklung und im Betrieb identisch; es unterscheidet sich nur die Adresse: http://127.0.0.1:8099/administration in der Entwicklung, bei Docker Compose der Host-Port 8081, dahinter die Adresse des Reverse-Proxys.
- Administration öffnen und anmelden. In der Entwicklung
admin/admin(§12). Die Admin-Anmeldung hat ein eigenes Cookie und läuft nach 8 Stunden ab; ein Neustart des Prozesses meldet den Administrator ebenfalls ab. - «Konto anlegen» klicken. Das Formular hat nur drei Eingaben:
- E-Mail-Adresse — reiner Identifier für die Anmeldung. Es gibt keine Mail-Infrastruktur, es wird nichts versendet.
- Basisnummer — welche Anlage dieses Konto sieht, z. B.
58. Das ist die Anlage: eine eigene Anlagenverwaltung gibt es nicht. - Erster Tag unvollständig (optional) — wenn am ersten Betriebstag die PV-Messung schon lief, der Verbrauchszähler aber noch nicht. Der Tag wird dann in Tabelle und Fussnote gekennzeichnet.
- Passwort sofort notieren. Es wird erzeugt (vier Vierergruppen, ohne leicht verwechselbare Zeichen) und genau einmal angezeigt — mit einem Kopieren-Knopf. Es gibt keinen Weg, es später zu lesen; verloren heisst «Passwort zurücksetzen», und das verwirft alle Sessions des Kontos.
data_startnicht tippen. Der Beginn der Historie wird beim Anlegen automatisch mit einer Flux-first()-Abfrage gegen die Basisnummer ermittelt. Antwortet die Datenschicht gerade nicht, bleibt das Feld leer und lässt sich später im Detail nachtragen.- Anlage konfigurieren. Konto in der Liste öffnen, Abschnitt Dashboard-Einstellungen: Ortschaft (mit GeoAdmin-Vorschlägen, der Kanton folgt automatisch), Anzeigename, Anlagenleistung in kWp, Batteriekapazität in kWh, «Daten ab», Bezugspreis und Einspeisetarif in CHF/kWh, dazu Wappen- und Namenslogo. Das Detail hat zwei getrennte Formulare mit je eigenem «Speichern»: oben Konto (E-Mail, Basis, Teiltag), unten die Einstellungen.
- Gegenprobe. Abmelden, das Dashboard auf
/öffnen und sich mit E-Mail und dem notierten Passwort anmelden. Die Kennzahlen zu Geld erscheinen erst, wenn der jeweilige Tarif gesetzt ist; der spezifische Ertrag erst mit kWp.
Ohne kWp, Tarife oder Ortschaft funktioniert das Dashboard vollständig — die betroffenen Kacheln und die Wetterleiste erscheinen dann schlicht nicht. Nachträglich änderbar ist alles ausser dem einmalig angezeigten Passwort.
Ein Konto wird nie hart gelöscht, nur deaktiviert («Konto deaktivieren»); dabei verfallen sofort alle seine Sessions. Zwei Konten dürfen dieselbe Basisnummer tragen — sie teilen sich dann denselben Cache, weil es dieselbe physische Anlage ist (§4).