294 Commits (6bdab32b94ab8eec7a7cd3c8951e7a6ea696c584)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
42fd059fdd |
Office aus dem Verkauf nehmen, abschaltbar statt gelöscht
tests / pest (push) Waiting to run
Details
tests / assets (push) Waiting to run
Details
tests / release (push) Blocked by required conditions
Details
Office (ONLYOFFICE über einen künftigen gemeinsamen Dokumentenserver) wurde an drei Stellen beworben, ohne dass irgendwo im Repository ein Dokumentenserver existiert: der Kachel "Office im Browser" auf der Preistafel, dem Paketmerkmal `office` in Team und Business, und dem Zusatzmodul `collabora_pro` für 22,80 €/Monat. Alle drei sind jetzt stillgelegt, aber nicht gelöscht — ein einziger dokumentierter Schalter (LandingController::OFFICE_ON_SALE) plus eine neu veröffentlichte Planversion holen das Versprechen zurück, sobald der Dokumentenserver steht. Das Paketmerkmal wird über das im Katalog bereits etablierte Handover-Muster entfernt (neue Migration, analog zu switch_to_new_plan_ladder): die laufende Version von Team/Business wird geschlossen und durch eine identische ohne `office` ersetzt. Bestehende Verträge bleiben auf ihrer alten, eingefrorenen Version stehen und behalten das Merkmal unverändert. Die dritte Planversion für Team/Business hat 24 Bestandstests berührt, die eine feste Versionsnummer oder eine feste Preis-/Versionszahl annahmen — repariert, überwiegend durch dynamisches Lesen der aktuellen Version statt eines eingetippten Werts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
24eb7b3d80 |
Fix-Welle: Schlussreview-Befunde 1-6 zur Release-Decke
Sechs Befunde aus dem Schlussreview, in einer Welle behoben: - BEFUND 1 (Important): eine leere Auswahl im Festnageln-Feld liess pinRelease() ueber `$this->ceilingChoice ?: null` in setCeiling(null) laufen — die Gegenhandlung (Decke abnehmen) — und meldete dabei die Erfolgsmeldung des Festnagelns. ConfirmPinRelease::confirm() schickt die Version jetzt als Event-Nutzlast (wie ConfirmSaveSecret den Schluessel), und pinRelease(string $version) weist eine leere Version ausdruecklich ab, mit einer eigenen Meldung (release_pin_empty). - BEFUND 2 (Minor, durch 1 miterledigt): Modal und Seite lasen bisher zwei getrennte Eigenschaften ($version vs. $ceilingChoice). Der Fix oben beseitigt die Trennung. - BEFUND 3 (Important, Text only): der Kommentar bei release_tag_exists() in deploy/lib/release.sh und der Fehlerbehandlungs-Abschnitt der Spec behaupteten, ceiling_missing schuetze gegen einen vom Release-Prozess geloeschten Tag. Tut es nicht: `git fetch --tags --force` (ohne --prune-tags, bewusst) entfernt keine lokal bereits geholten Tags, die drueben verschwunden sind. Beide Stellen beschreiben jetzt, wogegen die Pruefung tatsaechlich schuetzt (ein nie geholter oder nie existierender Tag) und wogegen nicht. Kein --prune-tags hinzugefuegt. - BEFUND 4 (Minor): ConfirmPinRelease hatte keinen Test. Zwei neue Tests nach dem Vorbild von ConfirmSaveSecret in IntegrationsPageTest. - BEFUND 5 (Minor): ceilingChoice wurde nie aus dem gesetzten Zustand vorbelegt. UpdateChannel::ceiling() ist jetzt public, Settings::mount() belegt das Feld damit vor. - BEFUND 6 (Minor): eine von Hand geleerte Deckendatei liest die Konsole als "keine Decke" (ceiling() -> null), der Agent meldet dafuer aber ceiling_error. Der "Decke abnehmen"-Knopf stand nur hinter @if($update['ceiling']) und verschwand damit genau in dem Zustand, aus dem er zurueckfuehren muesste. Bedingung erweitert auf ($update['ceiling'] || $update['ceiling_error']). Jeder Befund traegt einen eigenen Test in ReleaseCeilingConsoleTest.php. Volle Suite: 2973 passed (10389 assertions). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
073e147ff8 |
Festnageln aus der Konsole
Task 5: Bedienoberfläche zum Festnageln einer Release-Version, in der bestehenden Update-Handlungsleiste statt einer zweiten daneben. Bestätigt im Modal (R23), Auswahlfeld selbst ohne Modal (R20). |
|
|
|
6de7bf9db8 |
Version 1.8.2 — die Zugangs-Plakette bricht nicht mehr mitten im Wort
tests / pest (push) Waiting to run
Details
tests / assets (push) Waiting to run
Details
tests / release (push) Blocked by required conditions
Details
Am Telefon brach "Mitarbeiter-Zugang" in der Plakette um und machte aus ihr einen doppelt hohen Kasten mit einem Bindestrich am Zeilenende. whitespace-nowrap. Das ist die kleinste der Beschwerden an dieser Zeile, aber die einzige mit einer eindeutigen Ursache. Die uebrige Mobilansicht — jede Angabe auf einer eigenen Zeile, Statusplakette und Knoepfe oben rechts weit weg von den Daten — ist eine Gestaltungsfrage und keine Klasse. |
|
|
|
d9280dae1e |
Version 1.8.1 — die Update-Knoepfe sind am Telefon eine Leiste, keine Streuung
tests / pest (push) Waiting to run
Details
tests / assets (push) Waiting to run
Details
tests / release (push) Blocked by required conditions
Details
flex-wrap allein liess die zwei Knoepfe in ihrer natuerlichen Breite unter dem Text stehen: zwei ungleich breite Kaesten, linksbuendig, die aussahen wie verrutscht statt wie eine Gruppe. Am Telefon jetzt volle Breite und gestapelt — eine erkennbare Handlungsleiste. Ab sm stehen sie wie bisher nebeneinander, dort ist Platz. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
df73e558e9 |
Kuendigung B1, Tasks 4+5: der Zeitplan-Griff und die Sichtbarkeit
Gekuendigte Kundenmaschinen liefen bisher fuer immer weiter. Task 3 hat den Abbau gebaut; hier kommen der Griff, der ihn faehrt, und der Ort, an dem man sieht, was passiert ist. Die Wartezeit-Frage, entschieden: ein Auftrag je Instanz auf der provisioning-Warteschlange. Das ist keine Abwaegung — nur queue-provisioning steht im Netz-Namensraum des vpn-hub, der scheduler-Container nicht. Ein Befehl, der selbst mit Proxmox spraeche, haette gar keine Route zu einem Host. Die Fristen stehen ausdruecklich ineinander: die Aktion bekommt 600+1200 = 1800 s, der Auftrag hat $timeout 2100 s, retry_after der Verbindung ist 2400 s. Nur die unterste Uhr hinterlaesst einen lesbaren Grund am Datensatz; die mittlere toetet den Arbeiterprozess stumm, die oberste startet einen ZWEITEN Abbau gegen eine Maschine mitten im vzdump. $tries=1, weil ein sofortiger zweiter Versuch am Herunterfahren einer gesperrten VM scheitern und den richtigen Grund ueberschreiben wuerde. Die Staffelung ist als Pruefung festgenagelt. Der Preis — zwanzig statt sechzig Minuten fuers Sichern — steht im Kopfkommentar ausgeschrieben. Dazu zwei Entscheidungen, nach denen niemand gefragt hat: eine Obergrenze je Lauf, weil ueber dieselbe serielle Warteschlange bezahlte Bestellungen laufen; und eine Reihenfolge, die einen Dauerfall die uebrigen nicht aushungern laesst. Zeitplan taeglich um 05:30 — der Abbau hat keinen Moment, auf den es ankommt, aber er darf nicht ins naechtliche vzdump-Fenster um 02:00 fallen. Sichtbarkeit: zwei Kaesten in der Konsole. „Abbau haengt" (rot, ganz oben) — eine Instanz mit gefuelltem teardown_error steht unbegrenzt und belegt weiter einen Platz. Und „Archiviert und abgebaut" (unter der Liste) mit archive_volid im Klartext. Beide sortieren absteigend und beide haben ein Ende: der Fehler raeumt sich beim naechsten erfolgreichen Lauf selbst ab, das Archiv faellt nach zwoelf Monaten heraus. Der Folgepunkt vom Export-Kasten also nicht noch einmal. Suite 2929 gruen. 25 neue Pruefungen, vier Mutationsproben rot gesehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
e80aac8b4a |
Version 1.7.2 — eine gescheiterte Anfrage ist kein Deployment
tests / pest (push) Has been cancelled
Details
tests / assets (push) Has been cancelled
Details
tests / release (push) Has been cancelled
Details
Auf dem Telefon erschien das Vollbild-Fenster "Aktualisierung laeuft" immer
wieder, obwohl nichts lief; nach einem Neuladen war es weg.
Die Ursache stand im catch des Waechters:
} catch {
this.wasRunning = true; // ohne jede Bedingung
Jede fehlgeschlagene Anfrage schaltete damit das Fenster ein — auch eine, die
mit einer Aktualisierung nichts zu tun hatte. Am Telefon passierte das
staendig: visibilitychange prueft sofort bei jeder Rueckkehr in den
Vordergrund, das Funkmodul ist dann noch nicht wach, die Anfrage scheitert.
Nach einem Neuladen war es weg, weil die Seite dann den echten Serverwert
mitbrachte — was es wie einen Geist aussehen liess statt wie den Fehler, der es
war.
Jetzt gilt eine gescheiterte Anfrage erst dann als Deployment, wenn vorher
bekannt war, dass eines laeuft (wasRunning oder serverConfirmed). Waehrend
eines echten Deployments ist das erfuellt — der Server meldet running, bevor
die Behaelter heruntergehen —, also bleibt das Fenster dort stehen wie bisher.
2879 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
37e1914246 |
Merge branch 'claude/nice-moser-521659'
# Conflicts: # VERSION # deploy/install-agent.sh # deploy/update.sh |
|
|
|
e68113dd02 |
Version 1.6.6 — die Seitenleiste klappt am Telefon nicht mehr auf und zu
tests / pest (push) Has been cancelled
Details
tests / assets (push) Has been cancelled
Details
tests / release (push) Has been cancelled
Details
Zwei Fehler, die nur auf schmalen Schirmen auffielen. Die Seitenleiste trug ihre Anfangsposition NUR in der Alpine-Bindung, nicht in den statischen Klassen. Bis Alpine startete, stand sie damit auf translate-x-0, also offen — und wurde danach hinausgeschoben. Zusammen mit transition-transform war das keine unsichtbare Korrektur, sondern eine sichtbare Fahrt: bei JEDEM Seitenwechsel klappte sie kurz auf und wieder zu. Jetzt steht -translate-x-full statisch in der Klassenliste; das erste Bild zeigt sie geschlossen, es gibt nichts mehr zu korrigieren. Geoeffnet wird ueber !translate-x-0 — mit Ausrufezeichen, weil bei zwei gleich starken Tailwind-Klassen die Reihenfolge im Stylesheet entscheidet und nicht die im Element. Und die zwei Update-Knoepfe liefen ueber den Rand: zusammen breiter als ein Telefon, und shrink-0 verbot ihnen zusaetzlich, schmaler zu werden. Jetzt flex-wrap, shrink-0 erst ab sm. 148 Layout-Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
4debc79883 |
Eine hängende Sperre ist jetzt ein Knopf, kein SSH-Zugang
Am 4. August 2026 hielt ein einziger Aufruf — `timeout 45 docker compose exec` ohne `--kill-after` — die Sperre stundenlang, und jeder folgende Lauf wurde übersprungen. Behoben ist die Ursache in v1.6.5; was fehlte, war der Griff für das nächste Mal. Er lag per SSH auf dem Wirt. Die Konsole kann ihn nicht selbst führen: sie ist www-data in einem Behälter, der Sperrenhalter ist ein Prozess auf dem WIRT. Also bittet sie den Agenten, und der ruft den root-eigenen Helfer — dasselbe Muster wie `apply-proxy-hosts`, mit eigener sudoers-Zeile. Die Bitte liegt in einer EIGENEN Datei, und das ist keine Geschmacksfrage: der Agent nimmt die Sperre in seinen ersten Zeilen, lange bevor er den Postkasten ansieht. Ein blockierter Lauf steigt vorher aus — eine Entsperr-Bitte im Postkasten erreichte ihn also genau dann nie, wenn sie gebraucht wird. Dazu nimmt der Postkasten eine Bitte zur Zeit an, und die dreißig Minuten, in denen dort eine wartende Update-Anfrage liegt, sind die, in denen jemand entsperren will. Der Dienstbenutzer sagt „gib die Sperre frei", nicht „töte 1234": welcher Prozess das ist, sucht der Helfer selbst. Dürfte der Anrufer die Nummer liefern, wäre die Freigabe das Recht, jeden beliebigen Prozess als root zu beenden. Beendet werden alle, die die Sperrdatei OFFEN halten — nicht nur der, der das flock genommen hat. Ein flock hängt an der offenen Dateibeschreibung, und die wird vererbt: stirbt der Agent, sein hängendes `docker compose exec` aber nicht, bleibt die Sperre gehalten. Genau das war der Vorfall. Verschont bleiben PID 1 und der Anrufer samt Vorfahren — im Betrieb hält der blockierte Agent die Datei selbst offen und ist zugleich der, der anruft. Erst SIGTERM, fünf Sekunden, dann SIGKILL. Eine Frist ohne Nachdruck ist keine Frist. Bestätigt wird im Modal (R23), das VORHER nennt, was es beendet — PID, Laufzeit und Kommandozeile aus dem Lebenszeichen. Eigene Berechtigung `deployment.unblock`, nicht `site.manage`: wer aktualisieren darf, darf damit nicht automatisch in einen laufenden Vorgang hineingreifen. Der Helfer-Vertrag steigt auf 3, und update.sh verlangt ihn. Ohne das wäre der Knopf auf einem Wirt, der install-agent.sh seither nicht mehr gefahren hat, ein Knopf, der still nichts tut — und die Konsole sagt es jetzt, statt den Betreiber dorthin zurückzuschicken, wo er ohne sie schon war. Geprüft wird ausgeführt, nicht begutachtet: der Helfer läuft im Test gegen eine echte Sperrdatei mit echten Prozessen daran. Das hat gleich einen Fehler gefunden — der Helfer erbt den Deskriptor und hielt seine eigenen Kommandosubstitutionen für Halter, sodass die Runde nie zum Ende gekommen wäre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
9fa45e869f |
Ein Terminal auf den CluPilot-Server selbst
Eigene Faehigkeit, eigener Schluessel, eigene Tuer. Die Bruecke bleibt unveraendert: sie kennt keine Hosts, nur Tickets. Den Weg zum Wirt traegt die root-eigene Haelfte des Updaters — sie holt sich die oeffentliche Haelfte selbst, prueft sie hart und schreibt die Optionsliste, die das Dienstkonto nicht bestimmen darf. Kein sudo-Weg dorthin: einen Root-Schluessel eintraegt ein Mensch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
c9fef59983 |
Fix-Welle: die Kuendigung sagt jedem nur das, was fuer ihn stimmt
Vier Befunde aus dem Gesamt-Review, und alle vier hatten dieselbe Wurzel:
`export_wish` fuehrt drei Zustaende, und jede Stelle, die den Kunden
ansprach, kannte nur zwei.
K1 — Der Streifen im Dashboard trug einen Schalter, und `(bool) null` ist
`false`. Wer vor dieser Ausrollung gekuendigt hat, las unter dem Streifen
„Kein Export gewuenscht" — als waere das seine eigene Antwort. Jetzt stehen
dort dieselben zwei Auswahlfelder wie im Kuendigungsdialog: gleiche Frage,
gleiche Form, und ein unbeantworteter Zustand markiert schlicht keines von
beiden. Der Satz daneben fragt dann, statt zu behaupten, und sagt, was
passiert, wenn die Frage offen bleibt.
K2 — Die Vertragsseite versprach jedem den Export, auch dem, den der Dialog
eine Sekunde vorher mit einem bewussten „Nein" genau dorthin umgeleitet
hatte. Drei Fassungen statt einer, an `export_wish` gebunden. Die Ja-Fassung
verspricht dabei nicht mehr den Export selbst, sondern dass der Wunsch
vermerkt ist und sich jemand meldet — den Export gibt es nicht, und ein
gebundenes, aber weiterhin unhaltbares Versprechen haette den Fehler nur
verschoben.
Dazu der Zustand danach: eine `ended`-Instanz holt Dashboard::render() nicht
mehr, und der Kunde fiel in denselben Zweig wie jemand, der noch nie etwas
bestellt hat — „Ihre Cloud wird eingerichtet." samt „Paket buchen", am Tag,
an dem ihm die Adresse eingezogen wurde. Der Fall hat jetzt seinen eigenen
Kasten, mit dem Datum, an dem das Paket endete.
W1 — Die Erinnerungsmail behauptete im Praesens, wir bereiteten bereits einen
Export vor. Der Satz sagt jetzt, was stimmt. Und die Antwort hatte in der
ganzen Konsole keinen einzigen Leser: ein „Ja" landete in einer Spalte, die
niemand je zu Gesicht bekam. Ueber der Instanzliste steht deshalb ein
Abschnitt „Datenexport bestellt" — wer, und bis wann. Nicht als Plakette in
der Zeile, weil die Liste geblaettert ist und ein alter Eintrag auf Seite acht
saesse; nicht auf der Uebersicht, weil ein Hinweis, den nichts je wieder
abraeumen kann, Moebel waere.
W3 — Die einzige Pruefung zur Anzeige der Antwort konnte nicht fehlschlagen:
`x-ui.switch` rendert beide Woerter und ueberlaesst dem CSS die Auswahl, also
war `assertSee('Kein Export gewuenscht')` bei true, bei false UND bei null
gruen. Nachgewiesen mit einer Wegwerf-Pruefung gegen den alten Streifen:
dreimal derselbe Satz, dreimal gruen. Jetzt drei Pruefungen, je eine pro
Zustand, am `checked`-Attribut der Auswahlfelder.
Zu jedem der vier Punkte wurde der Fix kurz zurueckgedreht und die neue
Zusicherung rot gesehen; die Ergebnisse stehen im Bericht.
Ganze Suite: 2819 gruen, 2 rot — beide fremd. ReadinessPageTest scheitert an
`server.private_key` aus der parallel laufenden Terminal-Arbeit im selben
Baum; HostStepTest ist auf main vorbestehend rot (install-agent.sh traegt
CONTRACT=3, update.sh HOST_STEP_NEEDS=2, beide unveraendert).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
cfd1481797 |
Ein Streifen im Dashboard sagt, wie lange der Zugang noch steht
Eine Mail sieben Tage vorher kann im Postfach untergehen; die eigene Uebersicht oeffnet der Kunde ohnehin. Der Streifen nennt das Datum, die Restzeit und was danach geschieht — dass der Zugang zur Cloud endet, nicht dass irgendetwas geloescht wird. Das ist die Frist, die ihn betrifft. Und er traegt die Antwort zum Export: wer liest, dass die Zeit laeuft, will es sich im selben Atemzug anders ueberlegen koennen (Aufgabe 4) — bis zum Laufzeitende, danach nicht mehr, geprueft direkt an der Methode und nicht nur am Schalter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
e83b1d886a |
Eine Warnung, bevor die eigene Cloud den Kunden aussperrt
Ab dem Laufzeitende zieht EndInstanceService die Adresse ein — der Kunde kommt
ab dem Moment nicht mehr an seine Nextcloud. Wer selbst etwas herunterladen
will, muss es VORHER tun, und das wusste bisher niemand.
Die Auswahl ist bewusst "hoechstens sieben Tage" und nicht "genau sieben Tage":
ein Gleichheitsvergleich verfehlt jede Instanz, die zwischen zwei Laeufen
durchrutscht, und der Preis dafuer waere, dass jemand ausgesperrt wird, ohne es
gewusst zu haben.
Zusaetzlich zum Zettel: lang/{de,en}/mail_pace.php bekommen einen Eintrag fuer
ServiceEndingSoonMail, weil MailPacePageTest fuer jede Klasse in MailLane::all()
einen Anzeigenamen verlangt — ohne ihn waere die Suite rot.
|
|
|
|
ae8c8680bb |
Fix-Runde: echte Umlaute, und das Versprechen sagt "auf Wunsch"
tests / pest (push) Has been cancelled
Details
tests / assets (push) Has been cancelled
Details
tests / release (push) Has been cancelled
Details
Befund 1: neuer deutscher Text (Kommentare, ein Testname) benutzte ae/oe/ue/ss statt ä/ö/ü/ß — der Bestand schreibt mit echten Umlauten, das Original wird hier nachgezogen. Betroffen: die drei Kommentare in ConfirmCancelPackage.php, der Blade-Kommentar, sowie Kopfkommentare und ein Testname in CancelAsksAboutExportTest.php. Der Funktionsname kuendbareInstanz() und die Test-Fixtures (E-Mail/Subdomain "kuendigt") bleiben ASCII — ersterer ist wörtlich aus dem Zettel übernommen, letztere sind technische Werte wie jede andere Test-Subdomain im Bestand (acme, berger). Befund 2 (Entscheidung des Betreibers): cancel_point_export versprach den Datenexport unbedingt, direkt über einer Frage, die ihn an ein Ja knüpft. "auf Wunsch" eingefügt, in beiden Sprachdateien — der Satz sagt jetzt, was der Dialog tatsächlich tut, ohne sonst etwas am Text zu ändern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
7d041db5e5 |
Beim Kuendigen wird gefragt, ob der Kunde seine Daten will
Der Angelpunkt des ganzen Vorhabens: wer keinen Export braucht, loest keine
Arbeit aus und wartet auf nichts. Zwei Auswahlfelder und keine Checkbox — eine
Checkbox kennt keinen dritten Zustand, und "nicht angekreuzt" waere von "nein"
nicht zu unterscheiden.
Die Frage ist nicht ueberspringbar (#[Validate('required|boolean')], vor Stripe
geprueft wie jede andere Vorbedingung hier). Das macht drei bestehende
Erfolgspfad-Pruefungen zu ConfirmCancelPackage neu pflichtig in einem Feld, das
sie vorher nicht kannten — PackageCancellationTest, SettingsTest und
EndInstanceServiceTest setzen deshalb jetzt zusaetzlich exportWish, ohne dass
sich an ihren eigentlichen Zusicherungen etwas aendert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
abfa4890ac |
Die Einladenzeile im Portal springt nicht mehr
tests / pest (push) Has been cancelled
Details
tests / assets (push) Has been cancelled
Details
tests / release (push) Has been cancelled
Details
Arbeit der Parallelsitzung an der Mitarbeiterverwaltung, hier nur eingecheckt — nicht von mir geschrieben. `sm:items-end` richtete die drei Felder unten aus. Der Fehler unter der E-Mail und der Hinweis unter der Rolle machten ihre Zelle hoeher und schoben damit deren Eingabefelder nach oben: drei Felder auf drei Hoehen, und je nachdem, ob gerade ein Fehler stand, sprang die Zeile. Jetzt `items-start` mit gleich hohen Beschriftungen; Fehler und Hinweis stehen unter der Zeile, wo sie niemanden mehr verschieben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
cfbe339df0 |
Die Seitenleiste fragt zuerst, ob etwas auf dich wartet
Achtundzwanzig Eintraege in sieben Gruppen, und „System" war der Platz fuer
alles, was sonst nirgends hinpasste: Mail-Einrichtung neben einem
Rechtsdokument, neben den persoenlichen Kontoeinstellungen, neben der
Mitarbeiterverwaltung — und ganz unten die Seite, die sagt, was liegt. Der
Betreiber hat es so beschrieben: „offene Punkte ist der letzte Punkt, dann
Rolle drueber und Einstellungen wieder drueber".
Zwei Regeln ordnen es jetzt.
Was man einmal einrichtet, verlaesst die Leiste: neun Seiten liegen als
Kacheln hinter EINEM Eintrag (admin.setup), gruppiert nach dem, was sie
einrichten. Keine dieser Seiten wurde angefasst — sie behalten Route,
Berechtigung und Inhalt, es aendert sich nur der Weg dorthin. Damit ist der
Umbau rueckholbar.
Und die drei Seiten, auf denen etwas WARTET, stehen ganz oben, mit einer Zahl
daneben: das ist die Frage, mit der man eine Konsole oeffnet. Stoerungen sind
aus „Betrieb" nach oben gezogen und Zahlungsprobleme aus „Geld" — umgezogen,
nicht verdoppelt. Bei null faellt die Plakette weg, der Eintrag bleibt: eine
Seite, die verschwindet, sobald nichts offen ist, ist genau dann nicht
erreichbar, wenn man nachsehen will, ob wirklich nichts offen ist.
Zwanzig Eintraege statt achtundzwanzig, jede Seite genau einmal.
Zwei Dinge, die beim Verschieben kaputtgegangen waeren:
* Die neun verschobenen Seiten standen nicht mehr in console(). Damit war auf
ihnen KEIN Eintrag markiert (Codex R15, P2) und currentLabel() lieferte
null — die Brotkrume haette dort nur noch „Konsole" gesagt. Zwei Stellen,
eine Wurzel: die Kachelliste ist nach Navigation::setup() gewandert, wo
beide sie lesen, und isCurrent() haelt die Tuer markiert, solange man
dahinter steht.
* Die Versionszeile im Fuss stand als toter Text da, waehrend die Seite mit
dem Aktualisierungsknopf in die Einrichtung gezogen war. Gemeldet vom
Betreiber: „man sieht es nicht, ohne genau hinzuklicken." Sie ist jetzt der
Weg dorthin — und sagt in der Akzentfarbe, wenn etwas wartet. Wartet
nichts, bleibt sie grau: eine Zeile, die immer ruft, ruft nie.
Die drei Zahlen liegen fuer eine Minute im Zwischenspeicher. Diese Leiste
rendert auf JEDER Konsolenseite; ohne das waeren es vier Abfragen je
Seitenaufruf — eine Abgabe, die man erst sucht, wenn die Konsole zaeh ist.
Achtzehn Zusicherungen, darunter die, auf die es ankommt: keine der
achtundzwanzig Seiten von vorher ist verlorengegangen. Die Liste steht im Test
ausgeschrieben und nicht aus der Repository-Geschichte gelesen — ein Test, der
sich seine Erwartung aus demselben Repository holt, das er prueft, prueft
nichts.
Entwurf: docs/superpowers/specs/2026-08-04-konsolen-seitenleiste-design.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
d04a76b6ee |
Ein Rückweg aus der abgelaufenen Betreiber-Einladung
tests / pest (push) Has been cancelled
Details
tests / assets (push) Has been cancelled
Details
tests / release (push) Has been cancelled
Details
Läuft der 72-Stunden-Link ab, gab es keinen Weg zurück ausser der Shell — und die setzt dabei ungefragt auf Owner. Settings::resendInvitation() zieht für ein noch nie benutztes Konto (last_login_at ist null) ein neues Token und verschickt dieselbe Einladungsmail; der Broker macht das alte Token dabei von selbst ungültig. inviteStaff() läuft jetzt in einer Transaktion, damit eine Störung zwischen Kontoanlage und Mailversand keine für immer blockierte Adresse mehr hinterlässt. Dazu der Nachtrag am Testnachweis: die reflektierende Prüfung auf ein verstecktes Passwort steigt jetzt in Arrays ab statt sie zu überspringen, und prüft zusätzlich das gerenderte HTML statt nur die Komponenteneigen- schaften. Drei Kleinigkeiten: ein falscher Kommentarhinweis auf eine angeblich fehlende Übersetzungsdatei korrigiert, eine fehlende Zusicherung gegen einen rohen Statusschlüssel ergänzt, und ein toter throttle-Wert aus config/auth.php entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
7edd88b71c |
Ein Zugang zur Konsole, dessen Passwort niemand kennt
Bisher erzeugte Settings::inviteStaff() beim Einladen eines Betreiber-Mitarbeiters ein Passwort und zeigte es dem Inhaber einmal auf dem Bildschirm, damit er es "sicher weitergibt" -- fuer einen Zugang zur Konsole, die die ganze Flotte verwaltet. Der Kommentar dort nannte sich selbst eine Attrappe. Jetzt geht eine Einladung ohne Passwort hinaus: der Eingeladene bekommt eine Mail mit einem Link und vergibt sein Passwort selbst, ueber App\Livewire\Auth\OperatorSetPassword. Niemand -- auch der Inhaber nicht -- kennt je ein fremdes Passwort. Dafuer brauchte es einen eigenen Weg, den es fuer Betreiber noch nicht gab (R21: Konsole und Portal teilen keine Identitaet). Ein eigener Broker mit eigener Tabelle (operator_password_reset_tokens, config/auth.php), eine eigene Route in der Gast-Gruppe der Konsole (admin.invitation), und Operator::sendPasswordResetNotification() wirft jetzt, statt Laravels Vorgabe zu nutzen, die auf die Portalseite verlinkt haette. Der Einladungslink gilt 72 Stunden -- lang genug fuer ein Wochenende, kurz genug, dass ein altes Postfach nicht auf Dauer einen Schluessel zur Konsole haelt. Zwei-Faktor bleibt unberuehrt: die neue Seite meldet niemanden automatisch an, der Eingeladene durchlaeuft danach die normale Anmeldung samt ihrer bestehenden Zwei-Faktor-Pruefung. Die Route liegt hinter denselben Netz- und Hostwaechtern wie der Rest der Konsole (RestrictAdminHost, RestrictConsoleNetwork), ohne Sonderfall. OperatorInvitationMail reiht sich in den Versandtakt ein (MailLane::LOCKED, wie ResetPasswordMail und VerifyEmailMail -- ein Mensch wartet gerade) und ist damit in der Vorschau- und Versandtakt-Uebersicht der Konsole sichtbar, statt lautlos in die gedrosselte Spur zu fallen. Zwei Mutationsproben gegen die tragende Zusicherung durchgefuehrt (Bericht: .superpowers/sdd/betreiber-einladung-report.md): die erste (durch die Vorgaengersitzung) hielt fest, dass drei Zusicherungen fallen wuerden, waere das Konto sofort anmeldbar; die zweite -- das Passwort testweise wieder auf die Seite gebracht -- faellt exakt an der dafuer gebauten reflektierenden Pruefung ueber alle oeffentlichen Eigenschaften der Seite. Suite: 2771 passed (9683 assertions). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
d1755f5921 |
Der Update-Agent sagt, dass er lebt — auch wenn er nicht arbeiten kann
Gemeldet: „seit 1.5.1 muss ich nach jedem Update install-agent.sh fahren,
sonst kommt kein Update mehr". Die Konsole meldete „Der Update-Dienst auf dem
Server laeuft nicht" und schickte genau dorthin.
Er lief. Das Journal zeigt zwei Tage lang lueckenlos 57-59 Laeufe je Stunde.
Aber ueber zweiundachtzig Minuten hinweg startete und endete jeder Lauf in
DERSELBEN Sekunde, waehrend ein arbeitender Lauf zwei braucht: sie stiegen
alle sofort wieder aus, an der Sperre eines anderen Vorgangs — `flock -n 9 ||
exit 0`. Lautlos. Kein Journal-Eintrag (systemd sieht einen sauberen Lauf),
keine Zeile in der Statusdatei, nichts in der Konsole.
Und die Statusdatei war die einzige Lebendmeldung, die es gab. Sie wird erst
nach rund 190 Zeilen geschrieben — nach dem `git fetch` und nach einem
`docker compose exec`, beide ohne Zeitgrenze und beide unter der Sperre. Ein
Lauf, der davor aussteigt, hinterlaesst nichts, und nach zwanzig Minuten
schliesst die Konsole daraus, der Dienst sei tot. Sie schloss falsch, und die
Handlungsanweisung dazu aendert an einer gehaltenen Sperre nichts.
Drei Aenderungen:
* Ein Lebenszeichen (agent-alive.json) als ERSTES bei jedem Lauf, vor allem,
was blockieren kann. Zwei Zustaende: `running` heisst "ich habe die Sperre
und arbeite", `blocked` heisst "ich bin ausgestiegen" — mit `since` (seit
wann ununterbrochen) und `held_by` (wer, per fuser und ps).
* Zeitgrenzen: 45 Sekunden um den docker-exec, 120 um den git fetch. Ohne
sie wartet ein Abruf gegen eine tote Verbindung, bis das Betriebssystem
ihn nach vielen Minuten aufgibt — und haelt dabei die Sperre.
* Die Konsole liest das Lebenszeichen statt der Statusdatei. Der Unterschied
ist der Punkt: Status heisst "zuletzt ERFOLGREICH nachgesehen",
Lebenszeichen heisst "zuletzt ueberhaupt gelaufen". Ein blockierter Agent
gilt als lebendig, aber seine Zahlen zaehlen nicht mehr als aktuell —
`behind` und `target_release` fallen auf "unbekannt", statt eine Stunde
alte Auskunft als frisch auszugeben.
Statt "laeuft nicht" steht dort jetzt "laeuft, kommt aber seit HH:MM nicht an
die Arbeit", mit dem Prozess darunter.
Ein Agent von VOR dieser Aenderung schreibt die Datei nicht — fuer den gilt
weiter die alte Regel. Ihn dafuer fuer tot zu erklaeren waere derselbe Fehler
mit umgekehrtem Vorzeichen; ein Test haelt das fest.
Widerlegt und damit ausgeschlossen: Besitzrechte (Wirt, .env und Behaelter
fuehren alle 1001), das Ausführbar-Bit (100755 im Repo), systemds
Startdrosselung (seit v1.1.0 abgeschaltet), ein zwischengespeicherter Zustand
im Panel (es liest bei jedem Aufruf frisch) und eine Luecke im Timer (keine).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
aaf8697c3d |
Unter jeder Spur steht, was in ihr faehrt
„Ruhig / mail-ruhig" war dieselbe Auskunft zweimal, die zweite in Kleinschrift. Der Warteschlangenname stand da, weil ich ihn fuer nuetzlich hielt — er ist es, aber in docker-compose.yml und in MailLane, also dort, wo jemand ihn wirklich braucht. Auf der Seite, auf der ein Betreiber entscheidet, was wie eilig ist, sagt er nichts. An seiner Stelle steht jetzt, was in der Spur faehrt und warum sie so faehrt: wer auf die direkte wartet (jemand vor einem Bildschirm), was die wichtige traegt (Wartung, Abschaltung, Wiederanlauf), und warum die ruhige warten darf (eine Rechnung um zwei nachts ist dieselbe Rechnung). Nachgesehen, bevor der Name verschwand: die Bereitschaftsseite nennt die Spuren nirgends beim Warteschlangennamen — es geht nichts verloren. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
714f19d4d4 |
Der Versandtakt nennt die Mails beim Namen
Die Zuordnung zeigte class_basename(): „DormantAccountWarningMail" stand
dort, wo der Betreiber „Konto ohne Paket wird geloescht" sucht. Ein
Klassenname ist eine Auskunft ueber den Bauplan, keine ueber die Mail.
Die Namen liegen in lang/{de,en}/mail_pace.php und NICHT in MailCatalogue,
obwohl der sehr aehnlich klingt: der Katalog zaehlt Mail*arten* — vier
Mahnstufen, vier Eintraege, vier Absenderwege —, diese Seite verteilt
Mail*klassen* auf Spuren, und alle vier Mahnstufen sind EINE Klasse, die als
eine Zeile eine Spur bekommt. Zwei Listen, weil es zwei verschiedene Dinge
sind. Fehlt doch ein Name, steht der Klassenname da wie bisher statt einer
leeren Zeile — und ein Test haelt fest, dass keiner fehlt, in beiden Sprachen.
Der Warteschlangenname (mail-direkt und Geschwister) stand in der Wertspalte
zwischen Beschriftung und Eingabefeldern und las sich wie ein weiterer Wert.
Er steht jetzt als Unterzeile bei der Spur. Weg gehoert er nicht: es ist die
Zeichenkette, die der Arbeiter in docker-compose.yml abhoert.
Suite gruen, Codex ohne Befund. Der Namenstest faellt gegen die alte Fassung
— nachgemessen, nicht angenommen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
40f255565b |
Ein Knopf, der eine Mail verspricht, die niemand verschicken kann
Die zweite Haelfte von K4. Die Wanderung legt das Versandkonto an; solange es niemand ausgefuellt hat, bestand die Attrappe unveraendert fort — und sie ist nicht der Randfall, sondern der Normalfall: der Mailserver dieses Produkts entsteht gerade erst, das Postfach ist am ersten Tag garantiert leer. Die Einladung verschickt NEXTCLOUD, nicht CluPilot — nur so entsteht das Passwort dort, wo niemand sonst es zu sehen bekommt. Ohne eingerichteten Versand kommt dort aber nichts heraus, und `occ user:add --generate-password --email` gelingt trotzdem: Nextcloud legt das Konto an, versucht die Willkommensmail, protokolliert intern einen Fehler und beendet mit 0. Der Auftrag meldete Erfolg, die Zeile sprang auf "Eingeladen", die Meldung versprach einen Link, an dem der Mitarbeiter sein Passwort selbst setzt — und niemand bekam etwas. sendInvite() fragt jetzt GuestMailConfig::for($instance)->available(): ein reiner Blick in die Datenbank, kein Tunnel, keine Warteschlange, deshalb darf er auf der Seite stehen. Steht der Versand nicht, geht kein Auftrag hinaus, der Sitz bleibt unveraendert — insbesondere ohne Anmeldenamen — und die Meldung sagt, dass nichts verschickt wurde und woran es liegt. Drei Entscheidungen dabei: Der Knopf bleibt stehen, der Hinweis steht ueber der Tabelle. Ihn an jeder Zeile verschwinden zu lassen liest sich nicht als "geht hier gerade nicht", sondern als "das kann dieses Produkt nicht" — genau die Beschwerde, die schon einmal dazu gefuehrt hat, dass die Aktionsspalte immer gezeichnet wird. Und der Zustand ist voruebergehend: er endet, sobald der Betrieb das Konto ausfuellt. Die Wache steht VOR dem Ratelimit. Sonst haette ein Inhaber seine Versuche aufgebraucht, bevor ueberhaupt einer hinausgehen konnte. Ohne laufende Instanz greift sie gar nicht: dann scheitert der Auftrag ohnehin an `no_instance` und die Zeile sagt das im Klartext. Diese Wache gilt dem anderen Fall — die Cloud laeuft, nur der Versand fehlt. Das Anlegen bleibt offen: es verspricht ausdruecklich keine Mail, und der Hinweistext sagt das auch. Der Bereitstellungsschritt ist unangetastet und bleibt bei "nicht eingerichtet ist etwas anderes als kaputt". Fuenf Zusicherungen, drei davon gegen den zurueckgedrehten Fix rot gesehen; die uebrigen zwei sind Grenzpruefungen und muessen in beide Richtungen gruen sein. Acht Bestandspruefungen richten den Versand jetzt ueber eine eigene Hilfsfunktion ein — zwei davon haetten sonst aus dem falschen Grund bestanden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
3704620bb5 |
Ein Knopf, der nur wieder rot zurueckkommt, und eine Zeile, die niemand einlud
Zwei Reste aus dem letzten Fix, beide dieselbe Klasse: die Zeile behauptet etwas ueber den Gast, das dort nicht gilt. Der Wiederholen-Knopf an einer entzogenen, nie gespiegelten Zeile lief auf user:disable gegen einen Benutzer, den es nie gab — scheitert, dieselbe rote Zeile, beliebig oft. retry() traegt jetzt denselben nc_synced_at-Vorbehalt wie queueSync(), mit invite als Ausnahme, denn genau dafuer ist der Knopf da. Und gezeichnet wird er nur noch, wo der Server ihn auch annimmt. Nach einer Wiederaufnahme stand nc_state auf synced und status auf invited, also zeichnete die Spalte "Eingeladen" — fuer jemanden, den niemand eingeladen hat und dessen Konto im Gast weiter gesperrt ist. nc_state geht beim Wiederaufnehmen zurueck auf none; nc_username und nc_synced_at bleiben, denn an letzterem haengt das Entsperren beim folgenden Einladen. Eine Pruefung geht den ganzen Weg. |
|
|
|
02a5120b9c |
Der Weg zurueck geht durch die Vordertuer, und ein Fehlschlag bleibt sichtbar
Der letzte Fix hat zwei eigene Loecher gerissen, beide dieselbe Klasse wie das, was er schliessen sollte: die Oberflaeche versicherte etwas, das im Gast nicht eingetreten war. Entzogen stand VOR dem Fehlschlag, also verschwanden Grund und Wiederholen-Knopf genau dort, wo sie am noetigsten sind: scheitert das disable, ist das Konto weiter offen, waehrend die Zeile "entzogen" sagt. Es gibt keinen Wiederholungslauf und keinen Abgleich, und diese Seite ist die einzige, die nc_state anzeigt. Beides steht jetzt nebeneinander. Und die Meldung empfahl einen neuen Sitz, den die Eindeutigkeit von (customer_id, email) unmoeglich machte. addSeat() nimmt einen entzogenen Sitz derselben Adresse wieder auf — durch dieselbe Platzpruefung wie jeder neue, denn das ist der Grund, warum es die Vordertuer sein muss und kein Knopf an der Zeile. Der Auftrag entsperrt dabei, was revoke() gesperrt hat; user:welcome tut das nicht. retry() bekommt die owner-Wache nach, die bisher nur am Knopf davor hing. Und der Kommentar in mount() behauptet keine Heilung mehr, die nicht stattfindet. |
|
|
|
a569d2215b |
Entzogen bleibt entzogen, und der Inhaber laedt sich nicht selbst ein
Dass revoke() nicht mehr loescht, hat eine Kehrseite, die nirgends stand: der Umschalter in suspend() kannte nur zwei Zustaende und machte aus einem entzogenen Sitz beim zweiten Klick wieder einen aktiven — samt user:enable und vorbei an der Platzgrenze, die nur beim Anlegen geprueft wird. sendInvite() bot denselben Weg. Beide weisen 'revoked' jetzt ab, die Zeile traegt keinen Handlungsknopf mehr, und wieder aufmachen kann man sie gar nicht: der Weg zurueck ist ein neuer Sitz. Der Einladen-Knopf stand auch an der Inhaber-Zeile. Ein Klick vor der fertigen Bereitstellung haette spaeter ein zweites Konto in der Gruppe admin angelegt, neben dem echten — und mount() haette den Sitz danach nie wieder verknuepft, weil die Bedingung am Zustand hing statt am Anmeldenamen. queueSync() entscheidet ueber nc_synced_at statt ueber nc_state: ein Sitz, dessen Einladung an einem unerreichbaren Gast scheiterte, hat dort nichts, was man sperren koennte. Damit das keine Luecke reisst, vermerkt der Auftrag ein angelegtes Konto sofort, auch wenn die Rolle danach scheitert. |
|
|
|
4477a7699e |
Die Mailseite fragt drei Fragen statt eine Wand zu zeigen
Vier Karten in einer schmalen Spalte, und die letzten beiden allein zweiundzwanzig gleich aussehende Zeilen: fuenf Zwecke, siebzehn Mailarten, jede „Beschriftung, Auswahlfeld", darunter zweimal derselbe Speichern-Knopf. Wer die Zuordnung EINER Mailart aendern wollte, scrollte an allem anderen vorbei und fand am Ende eine Wand. Der Betreiber hat die Seite als unlesbar gemeldet. Jetzt drei Reiter nach dem Muster von Admin\Integrations, das aus demselben Grund umgebaut wurde — geteilt nach der Frage, die man mitbringt: WOMIT gesendet wird, WER sendet, WAS von wo rausgeht. Der offene Reiter steht in der Adresszeile, ein Neuladen landet dort, wo man war. Die eigentliche Wand war die Zuordnung, und zwar nicht wegen ihrer Laenge: dass die Wegwahl je Mailart auf den Zweck zurueckfaellt, stand nur als Satz in der leeren Auswahl — siebzehnmal derselbe Satz neben siebzehn Zeilen, die alle gleich aussahen. Jetzt steht jede Mailart unter ihrem Zweck: oben das Postfach des Zwecks, darunter eingerueckt, was davon abweichen darf. Die Regel ist die Form geworden. Die leere Auswahl nennt die Adresse, die stattdessen greift — und zwar die, die WIRKLICH greift. Ein Codex-Befund am eigenen Umbau: ein abgeschaltetes Postfach faellt in MailboxResolver::for() auf „System" zurueck, und writePurposes() erlaubt genau das. Das Etikett haette eine Adresse genannt, die nichts verschickt, an der Stelle, an der man nachsieht, um es richtig zu machen. Es fragt deshalb dieselbe Aufloesung, die beim Senden entscheidet. Ein Knopf statt zwei: Zwecke und Wegwahl sind fuer den Betreiber ein Vorgang. Die Reihenfolge ist die Zusicherung — die Zwecke werden geprueft und werfen, BEVOR eine einzige Wegwahl geschrieben ist, sonst hinterliesse eine abgelehnte Eingabe die halbe Zuordnung. savePurposes() und saveRoutes() bleiben, was sie waren. Beide Warnungen stehen ueber den Reitern: sie gelten fuer die ganze Seite, und wer nachsieht, warum keine Mail ankam, soll nicht erst den richtigen Reiter suchen. Vier bestehende Tests zeigen jetzt auf den Postfach-Reiter, fuenf neue kamen dazu — darunter der Regressionstest zum Codex-Befund, der gegen die alte Fassung faellt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
0e3a76a7cb |
Aus der Attrappe wird Verwaltung
Anlegen und Einladen sind zwei Vorgaenge. Einladen schickt einen Auftrag auf die Bereitstellungs-Warteschlange — die einzige, die einen Gast erreicht — und der Sitz zeigt danach, was WIRKLICH passiert ist, samt Grund und Wiederholen-Knopf. Ohne das drueckt der Inhaber wieder und wieder. Entziehen loescht nichts mehr. Ratelimit 10 je Kunde und 3 je Sitz pro Stunde, mit echter Restzeit in der Meldung statt stummer Verweigerung. Eine Pruefung verbietet user:delete im ganzen app/-Verzeichnis. |
|
|
|
1886bf2076 |
Sicht auf die Spuren, und ein Schalter, der die Drossel abstellt
Wenn die Drossel je klemmt, muss der Ausweg ein Klick sein und kein Deployment. Eigene Seite (admin/mail-pace) statt eines fünften Abschnitts auf admin/mail: die dortige Seite ist bereits "alles in einer Wurst", und diese hier beobachtet laufenden Betrieb statt etwas einzurichten — deshalb auch in der Navigation unter "Betrieb", nicht unter "System". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
f40b762413 |
Zwei Empfaengerkreise, und eine Wegwahl, die auch das Konto verschiebt
Fix-Welle nach dem Gesamt-Review, Punkte 4 und 5. Beide sitzen in denselben
Dateien, deshalb ein Commit.
DIE HOST-MELDUNG WAR WORTWOERTLICH DIE KUNDENMAIL (Punkt 4).
notifyHostManagers() verschickte dieselbe SecurityBlockMail. Deren Text ist fuer
den Kunden geschrieben ("bei Ihrer Cloud", "…koennen Sie die Sperre auf der
Sicherheitsuebersicht aufheben"), der Knopf zeigte auf portal.security — wo ein
Betreiber kein Konto hat und Host-Sperren nie erscheinen —, welcher Host
betroffen ist stand nirgends, und die Anrede blieb leer, weil $block->instance
null ist.
Die Mail kann jetzt beide Faelle. Unterscheidungsmerkmal in der Ansicht ist
$hostName: gesetzt heisst Host-Sperre, null heisst Instanz-Sperre. Ein zweites
Flag daneben waere eine zweite Wahrheit.
- Betreff nennt den Host.
- Eigener Textblock (mail_heading_host, mail_preheader_host, mail_intro_host mit
"fehlgeschlagene SSH-Anmeldungen am Host :host"), eigene Tabellenzeile
field_host, eigener Hinweis mail_note_host.
- Der Knopf zeigt auf admin.hosts.show, also auf die Host-Detailseite der
Konsole, wo die Host-Sperren mit ihrem Aufheben-Knopf stehen.
- Keine Anrede im Host-Fall — sie war vorher leer, und der Empfaengerkreis ist
ohnehin mehrere Personen.
- Der Fehlalarm-Satz ist ein anderer und sagt, was ein Betreiber wissen will:
bestehende Verbindungen bleiben unberuehrt, die Regel steht unter
"established,related accept".
R19: die zwei Kommentare, die fuer Host-Sperren Unwahres behaupteten, sind
richtiggestellt — der Kopfkommentar von SecurityBlockMail und der
MailCatalogue-Eintrag. Beide beschreiben jetzt zwei Empfaengerkreise. Ein
Kommentar mit falscher Begruendung haelt den Naechsten vom Nachsehen ab.
DIE WEGWAHL VERSCHOB DEN ABSENDER, NICHT DAS KONTO (Punkt 5).
SecurityBlockMail nagelte mailer('cp_system') fest und holte das Postfach ueber
die Wegwahl. MailboxTransport loeste weiter ueber den ZWECK auf und
authentifizierte mit dessen Zugangsdaten. Legte der Betreiber security-block auf
info@ — der ausdrueckliche Anlass fuer Abschnitt 3a der Spec —, ging die Mail
mit From: info@ durch die SMTP-Sitzung von no-reply@. Viele Server weisen das ab
("553 5.7.1 Sender address rejected: not owned by user"), und dann verschwindet
ausgerechnet die Mail, die er gerade umgelegt hat. Betraf ALLE Mailarten.
Die Wegwahl bestimmt jetzt auch den Mailer. Die zweitbeste Loesung — die Auswahl
in der Konsole auf Postfaecher zu begrenzen, deren Konto benutzt wird — war
nicht noetig.
- config/mail.php erzeugt AUS MailCatalogue::all() je Mailart einen Mailer
cp_mail_<mailart> mit transport, purpose und mail_key. Erzeugt statt
aufgelistet, weil der Katalog ausdruecklich die EINE Liste ist; eine zweite
hier waere die, die beim siebzehnten Mail vergessen wird. Genauso statisch wie
die fuenf Zweck-Mailer: ein reines Array, keine Datenbank, auch unter
config:cache unveraendert.
- MailboxTransport nimmt den mail_key entgegen und loest damit ueber DIESELBE
MailRoute::purposeOrMailbox() auf, die auch der Umschlag fragt. Ohne
Schluessel entscheidet wie bisher allein der Zweck — jeder Mailer, der keinen
hat, verhaelt sich unveraendert.
- Jede Mailart mit Schluessel benennt ihren Mailer ueber
MailCatalogue::mailer(). Dessen Rueckfall auf cp_<zweck> fuer einen
unbekannten Schluessel ist nicht Zierde: DunningNoticeMail baut seinen
Schluessel aus der Mahnstufe, und ohne den Rueckfall waere eine Stufe
ausserhalb 0-3 ein "Mailer [...] is not defined" und damit ein Totalausfall
statt eines falschen Absenders.
- Betrifft alle Mailarten: 13 Mailables, die CloudReady-Notification und die
cloud-ready-Vorschau. ContactRequestMail bleibt auf cp_support — es hat keinen
Schluessel im Katalog, weil es keine eigene Mailart ist.
MailSenderOwnershipTest ist mitgezogen und dabei STRENGER geworden. Die alte
Fassung verglich "From-Zweck == Mailer-Zweck"; jetzt prueft sie, dass der
Katalog dem Zweck zustimmt, den der Umschlag verlangt. Katalog-Drift war bis
hierher nur als Warnung im Docblock von MailCatalogue festgehalten und durch
nichts erzwungen. Ein Literal, das selbst kein Schluessel ist, wird als PRAEFIX
behandelt (der Mahnlauf) — dann muessen alle Schluessel dahinter zustimmen, und
es muss mindestens einen geben.
MailPreviewTest fragte den Zweck bisher, indem es 'cp_' vom Mailer-Namen
abschnitt. Das war eine Zusicherung ueber das Namensschema, nicht ueber die
Zusage; sie liest den Zweck jetzt aus der Konfiguration des Mailers selbst.
tests/Feature/Mail/SenderAddressTest.php gehoert inhaltlich zur parallel
laufenden Sitzung. Vier Zusicherungen darin pruefen genau den Mailer-Namen, den
dieser Commit aendert, also sind sie mitgezogen — die Datei war dabei sauber,
es steckt keine fremde unfertige Arbeit in diesem Commit.
Committet mit ausdruecklicher Dateiangabe am Zeilenende, weil eine parallele
Sitzung an derselben Ablage arbeitet und der Index fremde Arbeit enthalten kann.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
86477ad265 |
Der Aufheben-Knopf im Portal bekommt seinen Abstand zurueck
Icon und Beschriftung klebten aneinander: die Klassen fuer Anzeigeart, senkrechte Ausrichtung und Abstand fehlten als EINZIGE Stelle im Repo — jeder andere Knopf mit Icon und Text traegt sie. Es ist der zentrale Knopf der neuen Portalseite, der einzige, den ein Kunde dort ueberhaupt drueckt. Kein Test hat es gefangen: IconLayoutTest prueft Zeilenumbruch und Groesse, nicht die Luecke dazwischen. Gefunden hat es das Review beim Vergleich mit den Knoepfen im uebrigen Repo. Die beiden Konsolen-Ansichten haben denselben Knopf OHNE Icon, brauchen die Klassen also nicht — nachgesehen, nicht angenommen. Committet mit ausdruecklicher Dateiangabe am Ende der Zeile, weil eine parallele Sitzung an derselben Ablage arbeitet und der Index fremde Arbeit enthalten kann. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
51a97019a8 |
Sperren sehen und aufheben: Portalseite fuer den Inhaber, Abschnitte in der Konsole
Aufgabe 6 des Fruehwarnsystems. Der Inhaber sieht im Portal die Sperren SEINER Instanzen und hebt sie dort auf; der Betreiber sieht in der Konsole alle, an der Kunden- und an der Host-Detailseite, und auf der Uebersicht steht ein Hinweis, solange irgendwo eine Sperre aktiv ist. Aufgehoben wird ueber ein Bestaetigungs-Modal (R23), und das Modal mutiert nichts: es wirft ein Ereignis, das die Seite auffaengt und an ihre eigene Methode weiterreicht. Die Berechtigungspruefung bleibt damit an der einen Stelle, an der sie schon stand — noetig, weil ein Modal ohne die Middleware der Seite erreichbar ist (R20). Dazu die Berechtigung `instances.manage`, nach dem Muster der bestehenden `instances.restart`-Migration; Abrechnung und Read-only bleiben unberuehrt. ACHTUNG, was hier sonst noch drinsteckt und NICHT zu dieser Aufgabe gehoert: rund 150 Zeilen zum Versandtakt — das Merkmal `RidesALane`, vierzehn Mailables und `MailLaneRoutingTest`. Die stammen aus einer PARALLEL laufenden Sitzung an einem anderen Feature. Wie das hineingeriet: der Implementierer dieser Aufgabe brach vor dem Commit ab und liess seine fertige Arbeit ungespeichert im Baum. Ich habe sie dateigenau mit `git add <dateien>` vorgemerkt, um nichts Fremdes mitzunehmen — und dabei uebersehen, dass `git add` nur HINZUFUEGT: die andere Sitzung hatte ihre Arbeit bereits vorgemerkt, und `git commit` schreibt den ganzen Index, nicht nur das zuletzt Hinzugefuegte. Richtig waere `git commit -- <dateien>` gewesen. Nichts ist verloren, und die volle Suite ist auf diesem Stand gruen (2571). Aber diese Botschaft soll nicht behaupten, sie beschreibe alles, was hier steht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
2d979052d0 |
Benachrichtigung ueber eine gesperrte Adresse, hoechstens eine je Stunde
SecurityBlockMail geht bei einer Instanz-Sperre an die Kontoadresse des Inhabers, aus dem SYSTEM-Postfach wie NewDeviceSignInMail. Die Drossel sitzt in BlockAddress::notifyInstanceOwner() ueber Settings (kein neues Feld fuer etwas, das nach einer Stunde niemanden mehr interessiert) und wird VOR dem Versandversuch gesetzt. Ein Throwable beim Verschicken wird gemeldet und verschluckt: die Sperre steht schon, bevor ueberhaupt versucht wird zu verschicken, und ein kaputtes Postfach darf sie nicht rueckgaengig machen. Host-Sperren verschicken bewusst noch keine Mail: kein Muster im Repo, wie eine Betreiber-Meldung ihren Empfaenger findet (siehe Bericht). Route 'portal.security' minimal angelegt (Aufgabe 6 baut die echte Seite) - auf einem eigenen Pfad, weil sie sich mit der oeffentlichen /security-Seite sonst lautlos gegenseitig ueberschreiben, sobald Portal und Website ohne eigene Domain laufen (RouteCollection indiziert ueber Methode+Domain+URI, nicht ueber den Namen). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
5dcfee9957 |
Wegwahl je Mailart: welche Mail aus welchem Postfach geht
MailCatalogue haelt die eine Liste aller sechzehn Mailarten (Schluessel, Beschriftung, Vorgabe-Zweck), aus MailPreviews herausgezogen, damit es nur noch eine Stelle gibt, die beim naechsten Mailtyp vergessen werden kann. MailRoute sitzt darueber: ein Eintrag ist eine Ausnahme fuer GENAU diese eine Mailart, keine zweite Zuordnungsebene — ohne Eintrag oder bei abgeschaltetem Zielpostfach faellt sie unveraendert auf den Zweck zurueck, den MailboxResolver schon kennt. SendsFromMailbox bekommt dafuer einen optionalen $mailKey; alle bestehenden Aufrufer (inklusive ContactRequestMail, das mailboxAddresses selbst zusammensetzt) bleiben bei null und damit beim alten Verhalten. Jede Mailklasse und die CloudReady-Benachrichtigung nennen jetzt ihren Katalog-Schluessel. Die Konsole bekommt eine vierte Karte unter der Zweck-Zuordnung: eine Zeile je Mailart, ein <select> mit den aktiven Postfaechern und "wie der Zweck (...)" als Vorgabe. Der wichtigste Test schickt eine Mail ohne jeden Eintrag und prueft, dass sie exakt beim bisherigen Postfach landet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
5ae2dc41fc |
Ein Waechter, der aufraeumt — und die vier offenen Punkte aus dem Betrieb
DER WAECHTER. Ein abgebrochenes Update liess den Stapel halb unten liegen, den Tunnel weg und die Seite eine Stunde lang mit 500 antworten — bis jemand von Hand nachsah. Das wartet nie wieder auf einen Menschen. deploy/watchdog.sh laeuft jede Minute als systemd-Dienst AUF DEM WIRT, nicht in einem Container: ein Waechter im Container braeuchte den Docker-Socket hineingereicht (Root auf dem Wirt fuer jeden, der je hineinkommt) und waere genau dann tot, wenn man ihn braucht. Er nimmt dieselbe Sperre wie der Update-Agent und fasst waehrend eines Updates nichts an. Er kennt vier Fehlerbilder, alle vier heute wirklich passiert, und fuer jedes genau einen Griff: fehlende Dienste starten; Container, die einander nicht mehr finden, NEU ERZEUGEN (ein Neustart hilft da nicht); wg0 hochziehen; einen seit ueber einer halben Stunde haengenden Wartungsmodus beenden. Was er nicht kennt, protokolliert er und laesst es in Ruhe. `maintenance-hold` ist die Handbremse. Beides nachgemessen: Dienst gestoppt -> wieder da; wg0 abgeraeumt -> wieder oben. DREI LUECKEN, DIE DERSELBE VORFALL AUFGEDECKT HAT: 1. `phase()` machte `mkdir -p` ohne Fangnetz. Gehoerte storage/ nach einem frueheren Fehltritt root, starb das Update mit `set -e` an seiner ERSTEN Zeile, ohne eine einzige Ausgabe. Von aussen sah das aus wie "haengengeblieben"; in Wahrheit war es nach einer Millisekunde vorbei. 2. Ein zweiter Aufruf meldete "Already up to date" und tat NICHTS — der Checkout stand ja schon auf dem Ziel. Der Commit ist die falsche Frage; jetzt wird der Zustand gefragt: laeuft jeder Dienst, ist der Wartungsmodus aus. 3. Nach einer Netz-Umstellung reicht `up -d` nicht: nur neu gestartete Container haengen weiter am alten Netz, alle laufen, und trotzdem loest kein Name mehr auf. Jetzt `--force-recreate`. DIE VIER PUNKTE AUS DEM BETRIEB: - Provisioning zeigte 15/16 in der Liste und "16 von 16" in der Karte daneben, bei Status "Fertig" und 100 %. Die Liste rechnete `current_step + 1` — "dieser Schritt laeuft gerade" —, und bei einem fertigen Lauf laeuft keiner mehr. - Mahngebuehren standen in CENT im Feld. Wer eine Gebuehr eintraegt, denkt in Euro und tippt "5"; daraus wurden fuenf Cent, ohne Widerspruch. Jetzt Euro im Feld, Cent in der Datenbank, gerundet statt abgeschnitten ((int)(19.99*100) ist 1998). Und Fristen und Geld stehen in zwei eigenen Bloecken mit eigener Ueberschrift, die Einheit im Feld statt in der Beschriftung. - Die Bueroadresse stand in der Konsole neben dem Management-Netz mit dem Vermerk "nicht entfernbar" — sie kommt aber aus TRUSTED_RANGES in der .env und ist sehr wohl aenderbar. Beim naechsten Umzug des Anschlusses waere das ein Aussperren gewesen. Strukturell sind nur zwei Eintraege; alles andere steht jetzt als das da, was es ist, mit dem Weg heraus. - Ein Host-Zugang hiess weiter "pve-fns-1", waehrend der Host laengst "fsn-01" hiess. Die Zeile zeigt jetzt den Namen des Hosts, nicht den einmal gespeicherten — damit traegt sie jede kuenftige Umbenennung von selbst. Und die Frage "wofuer brauche ich Uptime Kuma": es ist benutzt — RegisterMonitoring legt fuer jede Kunden-Instanz eine Ueberwachung an, SyncMonitoringStatus holt den Stand, und ein Ausfall steht auf der Uebersicht. Ohne Kunden-Instanzen gibt es nichts zu sehen, was wie "unbenutzt" aussieht. Das steht jetzt in den Einstellungen, statt dort nur "API-Token und wo die Bruecke erreichbar ist". 2522 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
e3c781f067 |
Der Tunnel bekommt einen eigenen Container
Bisher wohnte wg0 im Container des Provisionierungs-Arbeiters, also im Abbild `clupilot-app`. Das wird bei fast jeder Freigabe neu gebaut — und ein neu gebauter Container bekommt eine neue Adresse im Compose-Netz, womit die Weiterleitung fuer UDP 51820 neu geschrieben wird und JEDE bestehende WireGuard-Sitzung abreisst. Der Tunnel hing damit am Veroeffentlichungstakt der Anwendung, und zusaetzlich daran, dass ein PHP-Prozess nicht abstuerzt. Jetzt gehoert der Netz-Namensraum einem eigenen Container `vpn-hub` mit eigenem, winzigem Abbild (Alpine plus wireguard-tools), das sich fast nie aendert. Provisionierungs-Arbeiter, Terminal-Bruecke, interner DNS und internes Gateway steigen dort ein, statt einer von ihnen den Namensraum zu besitzen. NACHGEMESSEN, nicht angenommen: App-Abbild neu gebaut, Arbeiter, Bruecke und Gateway per --force-recreate neu erzeugt — der Hub blieb Container 92e928cf53b0, wg0 und beide Zugaenge unangetastet, und nginx erreichte die Bruecke weiter (HTTP 426). Genau der Vorgang, der bisher jedes Mal alles abgerissen hat. Nachgezogen: - nginx spricht die Bruecke unter `vpn-hub:8082` an — dem Namen des Namensraum-Eigentuemers; ein Mitbewohner hat keinen eigenen DNS-Eintrag. - update.sh baut vpn-hub mit und haengt Nachbar-Neustarts und den conntrack-Griff an die Frage, ob der Hub WIRKLICH neu gebaut wurde. - update-agent.sh startet den Arbeiter nicht mehr neu, sondern signalisiert ihm. Diese Stelle laeuft unbeaufsichtigt hinter dem Knopf „Dienste neu starten" — wer den drueckt, rechnet nicht damit, sich selbst auszusperren. - Vier Meldungen in der Konsole rieten dem Betreiber, genau den Befehl von Hand auszufuehren, der ihm den Tunnel abreisst. Auch die sind korrigiert. - rescue-tunnel.sh und das Runbook zeigen auf den neuen Besitzer. Eine Kleinigkeit unterwegs, die ich falsch angekuendigt hatte: `[[ … ]] && x=true` bricht unter `set -e` NICHT ab — bash nimmt die linke Seite einer &&-Liste ausdruecklich aus. Nachgeprueft; die if-Form bleibt trotzdem, aus Lesbarkeit, und der Kommentar sagt jetzt den wahren Grund. 2509 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
894dcd309b |
Wortmarke breiter, und die Hostzeile steht wieder auf einer Linie
tests / pest (push) Failing after 12m48s
Details
tests / assets (push) Successful in 21s
Details
tests / release (push) Has been skipped
Details
Zwei Dinge, die dem Betreiber am fertigen Bild aufgefallen sind. DIE WORTMARKE. Sechs Zellen pro Buchstabe liessen sie hoch und schmal wirken; acht treffen die Proportion. Damit ist die Zeichnung 65 statt 51 Zellen breit, und bei 13px rund 500px — auf dem Schirm eine Wortmarke, kein Etikett. Am Telefon 7px und damit rund 270px, was neben den Raendern hineinpasst. DIE HOSTZEILE. „Online" und „Verbunden" standen 3px versetzt, obwohl beide Zellen gleich hoch sind (18,6px, gemessen). Der Grund: ein `inline-flex` holt sich seine Grundlinie vom ERSTEN Kind. Links ist das ein 8px-Punkt, rechts ein 16px-Icon; beide sitzen in ihrer Zeile mittig, ihre Unterkanten liegen deshalb 4px auseinander, und genau darauf setzt der Browser die Grundlinie der ganzen Zelle. Mit `align-middle` richtet sich der Kasten nach seiner Mitte statt nach dem ersten Kind. Nachgemessen: vorher 361,3 gegen 358,5, jetzt beide 361,6 — Versatz 0px. Beides im Browser gemessen, nicht geschaetzt. 2508 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
8538fed0b4 |
Terminal: die Wortmarke steht gerade
Der Betreiber sah einen losen Klotz mitten im Wort. Ursache war der Bogen des `P` in der uebernommenen Figlet-Zeichnung: eine Zelle in der zweiten Zeile, unter der in der ersten nichts stand. Figlet-Schriften sind fuer echte Terminalschriften gezeichnet und tragen in IBM Plex Mono nicht. Jetzt selbst gezeichnet: jeder Buchstabe sechs Zellen breit (das `I` zwei), eine Zelle Abstand, und keine Zelle ohne Nachbarn darueber. Nicht von Hand abgezaehlt, sondern aus einer Buchstabentabelle gesetzt — von Hand hatte ich mich prompt verzaehlt und die Buchstaben liefen ineinander, was im Browser sofort zu sehen war. Groesser als vorher (15px statt 11px, am Telefon 10px): die neue Zeichnung ist 51 Zellen breit statt 62, bei gleicher Schriftgroesse wirkte sie deshalb kleiner. Zwischendurch stand hier „CluPilot Cloud" mit grauem zweiten Wort wie im Logo. Auf Wunsch des Betreibers wieder raus — nur die Wortmarke. 2508 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
92ebd5d545 |
Terminal: das Fenster sagt jetzt selbst, ob die Bruecke laeuft
Der Betreiber oeffnete ein Terminal und las "Keine Verbindung — laeuft der
Terminal-Dienst, und steht der Tunnel?". Das war keine Diagnose, das war eine
Rueckfrage an den, der gerade unterwegs ist und nicht nachsehen kann.
Der Grund ist eine Eigenart des Protokolls: scheitert ein WebSocket schon am
Handschlag, bekommt die Seite laut Norm KEINEN HTTP-Status — `event.code` ist
1006, sonst nichts. Das ist Absicht (sonst waere ein Socket ein Portscanner) und
macht ausgerechnet die Unterscheidung unmoeglich, auf die es hier ankommt: laeuft
die Bruecke nicht, oder ist die Leitung weg? Beides sah gleich aus.
Eine gewoehnliche Anfrage an dieselbe Stelle darf den Status sehr wohl sehen.
Scheitert der Socket, ohne dass je ein Byte kam, fragt das Fenster deshalb einmal
nach und liest die Antwort:
502/503/504 nginx erreicht die Bruecke nicht -> "Der Terminal-Dienst laeuft
nicht", mit dem Befehl, der ihn zurueckholt, und dem Hinweis auf
den geteilten Netz-Namensraum
404 oeffentlicher Hostname -> "Auf diesem Namen gibt es kein Terminal"
sonst die Bruecke lebt, die Sitzung ist an etwas anderem gescheitert;
"Keine Verbindung" bleibt stehen
gar nichts die Anfrage kam nicht einmal los -> die Leitung ist wirklich weg
Alle drei Faelle nachgemessen, nicht angenommen: Bruecke laeuft -> 426,
oeffentlicher Name -> 404, `docker compose stop terminal` -> 502. Und danach mit
gestoppter Bruecke im Browser angesehen, im selben Zustand, in dem der Betreiber
gerade steht.
Dazu ein Test, der jeden Schluessel abdeckt, den terminal.js an showStage()
uebergeben kann — ein fehlender schriebe "undefined" ins Fenster, und zwar
ausgerechnet in dem Moment, in dem etwas kaputt ist.
2507 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
4fc1ccd3db |
Terminal: die Bruecke ueberlebt jetzt ein Deployment
Aus dem Gesamt-Review, und der erste Befund ist der, der still zugeschlagen haette. `terminal` lebt im Netz-Namensraum von `queue-provisioning`, und ein Prozess bleibt in dem Namensraum, in dem er gestartet ist. `update.sh` startet den Hub neu — danach lauscht die Bruecke in einem, den es nicht mehr gibt. Nichts meldet dabei einen Fehler: `docker compose ps` sagt weiter "healthy", weil die Lebendpruefung ueber Loopback INNERHALB des verwaisten Namensraums laeuft. Nach aussen antwortet nginx mit 502, und der Betreiber liest "Keine Verbindung — laeuft der Terminal-Dienst?", waehrend der Dienst behauptet, es gehe ihm gut. Genau dieselbe Falle, die zwei Bloecke tiefer schon fuer vpn-dns/vpn-gateway behandelt ist; die Bruecke fehlte in der Behandlung. Nachgemessen statt geglaubt: Hub neu gestartet -> Docker sagt "healthy", curl aus dem Namensraum bekommt gar keine Antwort. Nach `restart terminal`: 200. Und ein zweiter Ausrollfehler daneben: gebaut wurde nur `app`. `docker compose up -d` baut nur Images, die es noch GAR NICHT gibt — beim ersten Ausrollen faellt das nicht auf, danach nie wieder. Eine Aenderung an docker/terminal/ saehe ausgeliefert aus, und es liefe das alte Image. Ausserdem: - Die Meldung zu 4502 zaehlte zwei Ursachen auf, der Code deckt fuenf. Die Bruecke schickt 4502 fuer JEDE gescheiterte Anmeldung, auch fuer einen abgewiesenen Schluessel — und das ist der wahrscheinlichste Fall, wenn ein Host neu aufgesetzt wurde. "antwortet nicht" war dort schlicht falsch: die Maschine hat geantwortet und abgelehnt. Titel und Text legen sich nicht mehr fest. - R19: der Kommentar an der Kopfzeile der Spalte nannte "Berechtigung, Betriebsbereitschaft" als Grund, warum der Knopf nicht ueberall steht. Letzteres entscheidet seit dem Entsperren nichts mehr, und zwanzig Zeilen tiefer begruendete der Kommentar am Knopf ausfuehrlich das Gegenteil. - Der Kommentar am Retry-Knopf erklaerte die Reihenfolge von .hidden gegen .inline-flex fuer zu unsicher, waehrend die Buehne dreissig Zeilen hoeher genau darauf baut. Tailwind gibt .hidden als letzte Display-Klasse aus; der wahre Grund fuer den Wrapper ist, dass die Klassen des Knopfes aus einem geteilten Bauteil kommen. - REDIS_URL stand fest auf Datenbank 1, waehrend PHP REDIS_CACHE_DB liest. Wer die anfasst, legt auf der einen Seite ab, wo die andere nicht sucht. 2507 Tests gruen, compose config und bash -n sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
f64a564c40 |
Terminal: keine Fehlerseite mehr, ein Knopf auf jeder Zeile, eine Buehne statt roter Zeilen
Drei Dinge, die beim ersten Hinsehen im Betrieb auffielen.
1. Der Knopf war weg. Er verschwand, wenn dem Host die Tunneladresse oder der
Fingerabdruck fehlte — mit der Begruendung, ein Knopf, der verlaesslich in
eine Ausnahme laeuft, sei schlechter als gar keiner. Das stimmte, solange die
Seite dahinter mit Laravels Fehlerseite aufging. Jetzt steht er auf jeder
Zeile: ein fehlender Knopf sah aus wie "hier gibt es kein Terminal" statt
"hier noch nicht, und zwar deshalb".
2. Die Seite ging mit einem Stacktrace auf. `TerminalTicket::issue()` warf,
niemand fing es, und wer den Knopf drueckte, bekam Klassenname, Dateipfad,
Zeilennummer und Quelltextauszug in einem Fenster des eigenen Produkts.
`blocker()` beantwortet die Frage jetzt VOR dem Ausstellen und gibt ein
Merkwort zurueck, keinen Satz — die Formulierung gehoert in die
Sprachdateien. `mount()` wirft nicht mehr, mit Fangzaun fuer das, womit
niemand gerechnet hat.
3. Der Abbruch war die einzige ungestaltete Stelle im Produkt: eine rote
ANSI-Zeile mitten in der eigenen Ausgabe. Der Vorspann und der Schirm waren
Geschwister, von denen abwechselnd eines `hidden` trug — das trug genau
einmal, beim Aufbau, und fuer alles danach fehlte die Rueckfahrkarte. Die
Buehne liegt jetzt UEBER dem Terminal und kann dreimal auftreten: beim
Verbinden, beim Ende, beim Abbruch. Die Sitzung darunter bleibt stehen.
Welcher Text, entscheidet der Schliesscode der Bruecke (4401 Ticket, 4502
kein SSH); dazu ein Knopf, der neu laedt, weil ein Ticket dreissig Sekunden
gilt und genau einmal.
Beim Hinsehen gefunden, nicht beim Testen:
- Die Schriftgrafik war unlesbar. Die Figlet-Zeichnung setzt darauf, dass der
Unterstrich einer Zeile den Strich der naechsten beruehrt; in IBM Plex Mono
sitzt er tiefer. Eng verschmierte das Wort, weit zerfiel es. Vollbloecke
fuellen ihre Zelle und stapeln in jeder Schrift.
- Dunkelrot auf Fast-Schwarz hatte kaum Kontrast. Die Wortmarke bleibt jetzt
immer in der Akzentfarbe — sie ist keine Statuslampe, was los ist, sagt die
Zeile darunter.
- Auf dem Schirm stand ":host antwortet nicht". Der Name war an die Erklaerung
uebergeben, an die Ueberschrift nicht. Ein Test mit
`toContain(__('...title'))` haette das nie gefunden — er verglich ":host" mit
":host". Der neue prueft das Ergebnis.
Nachgewiesen: Knopf oeffnet ein NEUES Tab (die Liste bleibt stehen), Ticket
ausgestellt, Socket verbunden, Bruecke kommt nicht auf den Host, schliesst 4502,
Buehne kommt mit "pve-fsn-1 antwortet nicht" und Knopf zurueck, Knopf laedt
wirklich neu und holt ein frisches 64-Zeichen-Ticket. Der Fingerabdruck dafuer
war geliehen und ist wieder entfernt.
2507 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
c6403bf829 |
Terminal-Bruecke: ein Container, eine Aufgabe, ein Ticket
Der Beiwagen nach dem Vorbild von kuma-bridge: Python, ein Zweck, kein
Laravel. Er steht im Netz-Namensraum von queue-provisioning, weil dort wg0
lebt und nur von dort ein Host ueberhaupt erreichbar ist.
Drei Punkte, die das Review von Aufgabe 2 offen gelassen hat, sind hier
entschieden:
- Das Ticket reist als Sec-WebSocket-Protocol, nicht in der Adresszeile.
Die Adresse eines Upgrade-Antrags schreibt jeder Reverse Proxy mit, und
auf der Strecke stehen zwei, von denen nur einer aus diesem Repo
konfiguriert wird.
- /terminal/ws bleibt an der Wurzel, unabhaengig von AdminArea::prefix():
nginx kann die Konfiguration der Anwendung nicht lesen, der Ort muss ein
fester Text sein. In beiden Dateien steht die Begruendung.
- Die Bruecke sendet ausschliesslich Binaerrahmen.
Abweichungen vom Plan, jeweils gemessen statt vermutet:
- Der Schluessel in Redis traegt REDIS_PREFIX (clupilot-database-), nicht
CACHE_PREFIX: TerminalTicket schreibt ueber Redis::connection('cache'),
und phpredis stellt die Praefix-Option dieser Verbindung voran.
- Port 8082 statt 8081: im selben Namensraum lauscht der VPN-Gateway schon
auf VPN_HEALTH_PORT, und der zweite Zuhoerer auf einem Port stirbt.
- SSH ueber paramiko.Transport statt SSHClient. SSHClient prueft gegen
known_hosts, die dieser Container nicht hat und aus einem Fingerabdruck
auch nicht bilden kann; RejectPolicy verbaende nie, AutoAddPolicy
meldete sich mit einem Root-Schluessel an, bevor irgendetwas geprueft
ist. Der Transport erlaubt die richtige Reihenfolge: Handschlag,
Fingerabdruck, dann erst Anmeldung.
- Der Fingerabdruck wird gebildet wie in PhpseclibRemoteShell, nicht wie
bei OpenSSH: gehasht wird "<algorithmus> <base64-blob>", nicht der Blob.
- proxy_pass ueber eine Variable mit resolver, damit nginx nicht beim
Start scheitert, wenn die Bruecke gerade nicht laeuft.
- /terminal/ws antwortet auf einem oeffentlichen Hostnamen mit 404,
dieselbe Regel wie /admin eine Zeile darueber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
4e52c35f24 |
Terminal Fix-Runde 1: Ticket-Zusicherung im Test, ehrliche Kommentare, ein Ereignis eine Meldung
- Der Vorspann-Test prüft jetzt data-terminal-splash und die Ticketform (64 Hexzeichen) selbst, statt sich auf ein aria-label zu verlassen, das nur wegen des Tests existiert. - Neuer Test: Berechtigung vor Nachschlagen — eine erfundene UUID meldet 403, nie 404. - Der Knopf-Sichtbarkeitstest sichert jetzt auf die konkrete Terminal-Route zu statt auf das nackte Wort "Terminal" irgendwo auf der Seite. - Drei Kommentare (terminal.js, bare.blade.php, vite.config.js) behaupteten, die Seite liefe ohne Livewire/Chart.js — sie ist aber eine Vollseiten- Livewire-Komponente und zieht app.js über <x-shell.head> ohnehin mit. Kommentare korrigiert: eigener Einstiegspunkt, damit der Terminalcode nicht in app.js landet, nicht weil die Seite ohne Livewire liefe. - terminal.js: ein fehlgeschlagener Socket feuert error UND danach close; onclose schweigt jetzt, wenn nie ein Byte ankam, statt "Verbindung beendet" hinter "Verbindung nicht möglich" zu schreiben. - terminal.js: Textrahmen landen jetzt als String im Terminal statt als leeres Uint8Array. - data-host wird jetzt gelesen und steht in den Verbindungsmeldungen. - wire:ignore auf dem Terminalschirm, bevor die Komponente ihre erste Aktion bekommt und xterms DOM beim nächsten Render löscht. - hosts.blade.php/host-detail.blade.php: der Terminal-Knopf trägt sein href jetzt selbst (x-ui.button :href), statt in einem <a> zu stecken — interaktiver Inhalt in einem Link war ungültiges HTML. Suite: 2502 bestanden (vorher 2501 + ein neuer Test). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
941950584c |
Terminal: eigenes Fenster mit Vorspann, Knopf in Liste und Detailseite
Aufgabe 2: alles, was der Betreiber sieht, noch ohne Container dahinter. Der Knopf bleibt fuer einen Host ohne Tunneladresse oder Fingerabdruck absichtlich unsichtbar, statt in eine unbehandelte RuntimeException aus Aufgabe 1 zu fuehren. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
325e69f8f0 | Host-Liste holt sich ihren Stand selbst; die Ausstattung braucht keine Zeile nur fuer die Adresse | |
|
|
d8aee6ec2f | Zwei Attrappen: ein Banner ohne Hintergrund und Rechte, die ihren Schluessel statt ihrer Beschreibung zeigten | |
|
|
06d87f2111 | Offene Punkte: was fehlt und was folgt nebeneinander, statt auf halber Breite zu enden | |
|
|
d7bb0f2e63 | Offene Punkte: nach Dringlichkeit gruppiert, in der Formensprache der Bereitschaftsseite | |
|
|
07c51474d3 | Update drehte sich im Kreis: es ersetzte sich selbst mitten im Lauf; dazu eine Seite fuer offene Punkte |