Commit Graph

6 Commits

Author SHA1 Message Date
Scarriffle
8308536227 Phase 8: Ablage, AirDrop und Calendarr
Calendarr liefert Termine über den Gruppencontainer. Streng lesend: keine
eigene Anmeldung, kein Netz, kein Schreiben zurück.

Die Abdeckung ist der Kern und kein Detail. Der Schnappschuss reicht 49
Tage nach vorne, und das steht nirgends in der Datei — es ist zugesagtes
Verhalten des Schreibers. Außerhalb liegt keine Information vor, nicht
keine Termine; das Monatsraster graut solche Tage aus, statt „frei" zu
behaupten. Verlockend wäre, die Abdeckung aus dem frühesten Termin
abzuleiten. Das ist falsch, und ein Test hält es fest: mehrtägige Termine
ragen ins Fenster hinein und werden mitgeschrieben, obwohl ihr Anfang
nicht abgedeckt ist.

Bei „beide Quellen" ist die Abdeckung der Schnitt, nicht die Vereinigung.
Ein Tag gilt nur als bekannt, wenn ihn jede Quelle abdeckt. Umgekehrt
darf ein Ausfall einer Quelle die andere nicht mitreißen: Calendarr nie
geöffnet zu haben knipst den Apple-Kalender nicht aus.

Die Ablage kopiert, statt zu verweisen. Anhänge aus Mail oder WhatsApp
liegen in temporären Verzeichnissen — ein Verweis wäre in Stunden tot,
und die Ablage wäre leer, wenn man sie braucht. Aus demselben Grund nimmt
die Fläche UTType.item statt public.file-url an: solche Anhänge kommen
als Dateiversprechen, und loadFileRepresentation löst beides ein. Die
übergebene Datei existiert nur innerhalb des Rückrufs, also wird sie dort
sofort gesichert statt später auf dem MainActor.

Gelöscht wird in den Papierkorb. Automatisches Aufräumen ist standardmäßig
aus — eine Ablage, die ungefragt löscht, ist ein Papierkorb mit
Zeitschaltuhr.

Nebenbei: „mixer.on" und „network.vpn.active" standen als rohe Schlüssel
in der Oberfläche. Label(_:systemImage:) kennt kein Bundle und sucht immer
im Hauptbundle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:16:19 +02:00
Guido Schmit
d73b789fb7 Phase 6: Lüftersteuerung über privilegierten Helfer
Das Sicherheitsnetz kommt zuerst und liegt als reine Funktion vor — 17 Tests,
kein Hardwarezugriff nötig. Es sitzt im Helfer, nicht in der Oberfläche: die
kann abstürzen, hängen oder beendet sein, während die Lüfter auf einem festen
Wert stehen. Genau dafür ist es da.

Vier Regeln, nicht abschaltbar: nie unter das Firmware-Minimum, Rückfall auf
Automatik ab 95 °C, Rückfall wenn der Herzschlag fünf Sekunden ausbleibt,
Rückfall beim Beenden. Ohne lesbare Temperatur wird gar nicht gesteuert —
blind eine feste Drehzahl zu halten wäre die falsche Antwort auf fehlende
Information.

Die App sagt dem Helfer NICHT, wie warm es ist. Er misst selbst. Sonst hinge
das Netz an der Ehrlichkeit eines Prozesses, der abstürzen, hängen oder — bei
einer manipulierten Kopie — schlicht lügen kann. Der Herzschlag sagt nur "ich
lebe noch".

Der Helfer entscheidet jede Sekunde neu, nicht nur beim Setzen: nur so greifen
Wachhund und Temperaturwächter auch dann, wenn von der App nie wieder etwas
kommt. Geschrieben wird nur bei Änderung — jeder SMC-Schreibvorgang ist ein
Eingriff.

Der Schreibpfad existiert ausschließlich im Helfer, als eigene Kopie statt
geteilter Bibliothek. Was im App-Prozess nicht vorhanden ist, kann dort auch
nicht versehentlich aufgerufen werden.

XPC prüft die Signatur in BEIDE Richtungen. Ohne das könnte jedes Programm auf
dem Rechner die Lüfter eines root-Dienstes steuern — der Mach-Dienst ist
systemweit sichtbar.

Drei Fallstricke beim Einbetten, alle nachgemessen:

Xcode signiert Kommandozeilenprogramme unter dem Dateinamen. Der Helfer hieß
damit "OnyxHelper" statt com.scarriffleservices.onyx.helper, und die App hätte
ihren eigenen Helfer abgelehnt. Behoben über eine in die Binary eingebettete
Info.plist (CREATE_INFOPLIST_SECTION_IN_BINARY).

Der Build-Schritt läuft nach Xcodes Signatur; das Kopieren bricht das Siegel
des App-Bundles. Also wird zum Schluss neu signiert — mit denselben
Entitlements, sonst gingen App-Group, WeatherKit und die TCC-Berechtigungen
verloren.

Mit deklarierten outputFiles übersprang Xcode den Schritt, obwohl sich das
Programm geändert hatte.

Geprüft: App-Siegel gültig (deep), beide Signaturanforderungen erfüllt,
alle sieben Entitlements erhalten. 162 Tests grün.
2026-08-10 22:49:41 +02:00
Guido Schmit
a4cc75ab6e CPU und GPU zwischen Auslastung und Temperatur umschaltbar
Die M-Serie-Konvention gilt auch auf dem M5: Tp* sind die CPU-Kerne, Tg* die
GPU-Cluster. Gezeigt wird der wärmste Punkt, nicht der Durchschnitt —
gedrosselt wird nach dem heißesten Kern. Gemessen: CPU 76 °C, GPU 70 °C.

Die Fühlerliste wird zur Laufzeit gesucht statt fest verdrahtet: welche es
gibt, hängt am Modell. Auf diesem Mac sind es 285 Temperaturfühler.

Zwei Leistungsprobleme, gefunden bevor sie jemandem auffallen konnten:

Die Suche geht alle 3486 Keys durch und braucht knapp eine Sekunde. Auf dem
Hauptthread wäre das eine spürbare Startverzögerung — sie läuft jetzt im
Hintergrund, bis dahin zeigt die Oberfläche "—" statt einer erfundenen Zahl.

Fünfzig Fühler zu lesen kostete 34 ms je Abtastung, bei 1 Hz also 3,4 % CPU.
Zwei Maßnahmen: Typ und Größe je Key werden zwischengespeichert (sie stehen in
der Firmware fest, sie bei jedem Lesen zu erfragen verdoppelte die
IOKit-Aufrufe), und Temperaturen werden höchstens alle zwei Sekunden neu
gelesen — sie bewegen sich in einer Sekunde ohnehin kaum. Ergebnis: 6 ms
Grundlast, 18 ms alle zwei Sekunden, im Mittel gut 1 %.

Dabei noch eine Eigenheit gefunden: die Byte-Reihenfolge im SMC ist nicht
durchgängig. Messwerte wie B0AV kommen little-endian aus dem Akku-Baustein,
die Metadaten des SMC selbst dagegen big-endian. #KEY little-endian gelesen
ergibt 2 651 652 096 statt 3486 — die Fühlersuche brach damit still ab und
alle Temperaturen blieben leer.

Lüfterdrehzahlen stehen jetzt im Sensor-Popover, mit Balken zwischen Minimum
und Maximum der Firmware: "2321 U/min" allein sagt niemandem, ob das viel ist.
Die Steuerung selbst kommt mit Phase 6 — Schreibzugriff auf den SMC verlangt
root — und der Popover sagt das auch.

145 Tests grün.
2026-08-10 22:05:38 +02:00
Guido Schmit
a198d88104 Dock-Eintrag nur solange ein Fenster offen ist
Im Hintergrund gehört Onyx nicht in den Dock — es hat kein Fenster, das ein
Klick öffnen könnte. Mit sichtbarem Einstellungsfenster ist das Gegenteil
richtig: als reine .accessory-App lässt sich das Fenster weder über den Dock
noch über ⌘-Tab zurückholen, wenn es hinter etwas anderes rutscht.

Deshalb wird beim Öffnen auf .regular geschaltet und beim Schließen zurück —
außer der Dock-Eintrag ist in den Einstellungen ausdrücklich gewünscht.

Das generische weiße Symbol im Dock kam nicht aus dem Bundle: CFBundleIconName,
CFBundleIconFile, AppIcon.icns und der Assets-Katalog waren alle in Ordnung.
Es war der Icon-Cache von Launch Services, der die App von den vielen Starts
her kannte, als es noch gar kein Icon gab. Neu registriert und Cache verworfen.
2026-08-10 21:29:09 +02:00
Guido Schmit
3d21cac3b7 Wetter: fehlende Ortungserlaubnis wird als solche benannt
"Wetter nicht abrufbar" war eine falsche Auskunft: abrufbar war es sehr wohl,
es fehlte nur die Erlaubnis. Derselbe Fehlertyp wie zuvor beim Kalender —
ein Zustand, den die App nicht kennt, landet im nächstbesten Sammelfall und
schickt den Nutzer damit in die Irre.

locationUndetermined ist jetzt ein eigener Zustand neben locationDenied. Der
Unterschied ist nicht kosmetisch: bei undetermined lohnt eine Anfrage, bei
denied fragt macOS nie wieder und es hilft nur der Weg über die
Systemeinstellungen oder ein fest gewählter Ort. Beide Fälle führen jetzt
dorthin, wo es weitergeht.

Die Anfrage kommt aus dem Berechtigungen-Reiter, nicht aus dem Hintergrund —
dieselbe Lehre wie beim Kalender: ohne Vordergrundfenster zeigt macOS keinen
Dialog, und die App wirkt kaputt, ohne dass irgendwo ein Fehler steht.

Aufgefallen war es daran, dass im Protokoll überhaupt kein Wetter-Eintrag
stand. Der einzige Pfad zu "nicht abrufbar" ohne Protokolleintrag war der
Standort — jetzt protokolliert auch der.
2026-08-10 20:45:49 +02:00
Guido Schmit
40bd83e49e Berechtigungsdialog und Einstellungsfenster reparieren
Zwei Fehler, beide durch Messen gefunden statt durch Raten — und einer davon
war meine eigene fehlerhafte Diagnose: `log show` wurde von zsh abgefangen, ich
hatte stderr umgeleitet und die Fehlermeldung nie gesehen. Meine Aussage "keine
TCC-Einträge, also wurde nie gefragt" stützte sich damit auf einen Befehl, der
gar nicht lief.

Einstellungsfenster: SwiftUIs Settings-Szene wird über den privaten Selektor
showSettingsWindow: geöffnet, dessen Name sich zwischen macOS-Versionen schon
geändert hat und der bei .accessory-Apps ohne Menüleiste unzuverlässig ankommt.
Der Menüeintrag tat schlicht nichts. Jetzt ein eigenes NSWindow.

Kalenderberechtigung, zwei Ursachen übereinander:

Xcode lagert im Debug-Build den Programmcode in eine eigene dylib aus (für
SwiftUI-Vorschauen). Das Hauptprogramm ist dann nur ein Rumpf, und TCC ordnete
die Anfrage dem Rumpf zu — kTCCServiceCalendar erreichte tccd überhaupt nicht.
ENABLE_DEBUG_DYLIB = NO behebt das; danach erscheint die Anfrage im Protokoll.

Danach immer noch kein Dialog: promptType 1, aber promptPolicy 0. TCC bereitet
den Dialog vor und entscheidet sich dagegen, weil keine App im Vordergrund ist,
an die er gehören könnte. NSApp.activate davor hilft nicht — Aktivierung wirkt
asynchron und ist beim Aufruf noch nicht durch. Deshalb wird beim ersten Start
das Einstellungsfenster auf dem Reiter Berechtigungen geöffnet: dort steht ein
echtes Fenster im Vordergrund, und der Nutzer weiß, warum gefragt wird.

Folgefehler dabei: ohne Berechtigung zeigte das Widget "Nichts mehr für heute"
— eine Aussage, die es ohne Zugriff gar nicht treffen kann. refresh() fragt
jetzt nicht mehr selbst nach und meldet ohne Berechtigung ehrlich `denied`.

Fehler aus requestFullAccessToEvents werden nicht mehr mit try? verschluckt.
2026-08-10 18:20:21 +02:00