Consolidated 2026-07-03 from the former WorkToBeDone.md (deleted 2026-07-20 — recoverable from git
history), Design Ideas/redesign-implementation-plan.md, Design Ideas/framework-port-capabilities.md,
and the pre-release gating decisions made on 2026-07-03. This is the single planning document for the
release; the remaining source documents stay as reference.
| Surface | State in MVP |
|---|---|
| Dashboard | ✅ Ships — read-only live overview (fan cards with mode indicator incl. Curve, thermal bars, power card). Cooling-profile presets render grayed / inert (feature-flagged off via DashboardModel.CoolingProfilesEnabled). |
| Thermal Telemetry | ✅ Ships (redesigned, done) |
| Power Telemetry | ✅ Ships (redesigned, done; incl. charge limits, PD ports) |
| Fan Curve Profiles | ✅ Ships — the one and only fan control surface (Auto/Manual/Max/Custom curve, staging + preview safety holds) |
| Device Capabilities | ✅ Ships (redesigned, done) |
| Modules | ❌ Tab disabled (MainModel.IsModulesEnabled = false) until the FFI slot-reporting gaps are closed |
| Settings | ✅ Ships (see support matrix below) |
| Warnings & Issues | ✅ Ships (redesigned recovery hero, all 9 state variants) |
| Pane | Support |
|---|---|
| Service | 🟡 Reachability banner + Recheck: works. Restart/Shut down: works when the service is registered. Install/Update/Uninstall: wired but gated on packaged-helper discovery — in a dev checkout the packaged executable is not discoverable, so they stay disabled (see P0-1). Runtime configuration (polling intervals, allow-fan-control) Apply/Save/Reset: works. |
| Display units | ✅ Fully works. All 13 UnitsNet quantities, live samples, applies instantly app-wide, persists client-only to %LOCALAPPDATA%\SubZeroFramework\display-unit-preferences.json. |
| Startup & alerts | 🟡 Reworked 2026-07-18/19: "Start with system boot" (renamed from "Start with Windows") — now backed by the AutoLaunch NuGet library (AutoLaunchStartupRegistrationService, MIT, zero deps on modern .NET): HKCU Run key on Windows (verified end-to-end, incl. seamless migration of the pre-library Run entry via matching value name), freedesktop autostart on Linux, LaunchAgent on macOS for free. "Start minimized" removed (no tray icon — a hidden launch had no way back; old JSON key ignored). Autorun service: wired to enable/disable-autorun on both platforms, enabled once the service is registered — now exercisable in dev via the Debug-only packaged-helper fallback (see P0-1 note; the Warnings recovery page's "Install service" button verified enabled with it). Thermal alerts: WORK — toggle + monitor + toast delivery via DesktopNotificationsFixed (Windows toast user-confirmed 2026-07-19; Linux D-Bus wired, validated in the P1 Linux pass), incl. a "Send test notification" button. |
| Licenses | ✅ Works. Build target generates 171 entries — 169 NuGet packages (full transitive closure of app + Core + GrpcContracts + Service) plus 2 vendored native components. 160 carry real license text: 64 reproduce the package's own embedded file (18 declared via the nuspec license element, 46 found by the root LICENSE scan), 94 come from SPDX templates in Build/LicenseTexts (MIT, MIT-0, Apache-2.0, BSD-3-Clause, CC0-1.0), 2 from Build/StaticLicenses. Gaps (2026-07-20 audit): 3 declare an SPDX id we ship no template for (LibVLCSharp + VideoLAN.LibVLC.Windows = LGPL-2.1-or-later, Svg.Custom = MS-PL); 6 declare no license at all and correctly read "Unknown license terms"; 2 expose only a licence URL. Note the report is TFM-invariant (the targets file regexes the whole assets file), so the Windows page also lists packages that ship only on Linux — avoid the word "shipped" in user-facing copy. |
| About | 🟡 SubZero version, live EC build + framework-dotnet version: work. framework-system-ffi-extensions / framework-system rows show "Bundled with framework-dotnet" until the library embeds component versions (P1-6). |
- Service install / update / uninstall / reinstall end-to-end.
Researched 2026-07-03: the architecture already matches Microsoft's documented best practice —
Microsoft.Extensions.Hosting.WindowsServices(AddWindowsService+ServiceName) handles the run-as-a-service side only (SCM start/stop handshake → gracefulIHostLifetimeshutdown, Event Log logging default); it deliberately does NOT register/deregister services. Registration is SCM tooling, and our--service-managementCLI already does exactly what the MS docs prescribe (sc.exe create/config/failure …restart/5000×3/deleteon Windows; systemd unit +systemctlon Linux). So this item is validation + gaps, not new plumbing:- ✅ Fatal-exit audit — DONE 2026-07-18:
FrameworkFatalExitHandler(restore fans viaStopTelemetryLoops, thenEnvironment.Exit(1); no-ops while the host is already stopping so clean stops never read as failures). Wired into the curve worker's two Rx stream-fault handlers (previously log-only — a faulted stream left actuation dead while an EC override could still be applied) and a host-crash catch inProgram.Main(log critical → restore → return 1). Unit-tested incl. the restore-before-terminate ordering (FrameworkFatalExitHandlerTests). Recovery engagement end-to-end (SCM actually restarting on the non-zero exit) is validated by #2's induced-failure soak. - ✅
HostOptions.ShutdownTimeout— set explicitly to 90 s (was default 30 s) matching the systemd unit'sTimeoutStopSec=90; on .NET 8+WindowsServiceLifetimerequests the same additional stop time from the SCM. Fan restore itself is sub-second; the headroom covers a contended EC. - ✅ Packaged-helper discovery — RESOLVED by the shipped layout (verified 2026-07-26). The WiX MSI
installs the service to
INSTALLFOLDER\service-package\windows\SubZeroFramework.Service.exe, which is exactly the second candidateResolveWindowsServiceExecutablePathprobes, soPackagedHelperAvailableis true on every installed Windows build — the old "only true in CI artifacts" wording predates the installer. On Linux it is false by design: the UI payload prunesservice-package, andIsLinuxPackageManagedInstall()additionally withdraws Install/Update/Uninstall in favour of apt/dnf/pacman, while Restart, Shut down and autorun keep working through thesystemctl+pkexecfallbacks. Layout is documented indocs/INSTALL.md. The Debug-only dev-checkout fallback stays as-is (it is compiled out of Release so an installed app can never bind the SCM entry to a stray repo path). There is no post-install config override, and none is offered.ServiceControl:*binds only from an embeddedappsettings.jsonthat is never copied to the publish output, so those keys are build-time only — they exist to let a developer point a dev build at a custom path, nothing more. Do not describe them as a support escape hatch: the supported layouts are the MSI on Windows and the distro package (or the exact tarball layout indocs/INSTALL.md) on Linux. - Validate install → status turns healthy → update → uninstall → recovery page, on a clean machine, via both Settings → Service and the Warnings recovery page (UAC prompts included).
- Validate service account requirements for EC access on Windows (LocalSystem expected OK; verify).
- Decide shutdown ordering / preshutdown handling before update restart (carried ⏳).
- ✅ Fatal-exit audit — DONE 2026-07-18:
- Service stability under real hosting. During dev the console-hosted service died silently several times (signature of external termination of the dev host, not a crash). Before release: run as an installed Windows service for an extended soak; with the #1 fatal-exit fix, verify SCM restart + client reconnect actually engage on induced failure.
- Caller identity validation — DECIDED 2026-07-03: MVP ships with
HasCallerIdentityValidation=false. Rationale: transport is local-only Unix domain socket with expected-path validation, symlink/reparse protection, permission checks, and machine-scoped socket location; fan-control RPCs are fail-closed behind explicit service configuration; the UI already surfaces the authorization message and the Warnings "validation limited" state. Post-MVP hardening options:SO_PEERCREDon Linux; on Windows either named-pipe transport with client impersonation or socket-ACL ownership checks. ✅ Documented as the shipped posture inSubZeroFramework/Docs/IpcAuthorizationAndUiCadence.md(2026-07-18) — this item is now fully closed. - ✅ Thermal alerts — RESOLVED 2026-07-19 via
DesktopNotificationsFixed(user-suggested library), user-confirmed working on Windows. Delivery: Windows = classic Start-menu-shortcut + AppUserModelId registration with legacyWindows.UI.Notificationstoasts (DesktopNotificationsFixed.Windows, windows TFM); Linux =org.freedesktop.Notificationsover the session D-Bus (DesktopNotificationsFixed.FreeDesktop, desktop TFM, runtime-gated to Linux — validate live in the P1 Linux E2E pass). A "Send test notification" button on Startup & alerts fires through the exact alert path for one-click delivery verification. Dead ends recorded for posterity: WinAppSDKAppNotificationManager.Show()is silently dropped for self-contained unpackaged apps (registration succeeds, shell never accepts delivery — no Action Center entry, no per-appHKCU\...\Notifications\Settingsentry);ToastNotificationManagerCompatrejected (archived, critical NU1904 advisory viaSystem.Drawing.Common 4.7.0). - ✅ Versioning + release presentation — DONE 2026-07-18 (user decision:
0.1.0, single shared version).<Version>0.1.0</Version>inDirectory.Build.propsstamps UI, service, and Core alike (verified in the built assemblies);ApplicationDisplayVersiontracks it; About reads it viaAssemblyInformationalVersion. CI already stamps0.1.<run-number>(VERSION_PREFIX: '0.1'in ci.yml — keep it in sync with the props major.minor).CHANGELOG.mdrewritten with the full 0.1.0 MVP section. Update strategy: reinstall over the top (installer re-points the service via--service-management update); no in-app updater for MVP. - Manual deployment checklist for Windows (Linux can trail): install, autorun, update, uninstall,
recovery, fan-safety restore on stop/shutdown (checklist exists at
SubZeroFramework/Docs/FanSafetyShutdownChecklist.md— fold service lifecycle into it) (carried ⏳). ✅ Addressed 2026-07-26: the in-app Uninstall no longer deletes the SCM entry the MSI owns. On an installed Windows build it now runs the real uninstaller (msiexec /x{ProductCode}, product located via the stableUpgradeCodebecause WiX regenerates the ProductCode every build) and closes the app so its files can be removed — the package already stops and deregisters the service itself. On a development or extracted build, where there is no installer, the button keeps its old service-only behaviour and says so. Both paths now confirm first; the confirmation states that saved fan profiles survive, because the package deliberately keeps%ProgramData%\SubZeroFrameworkon uninstall. - Final release QA sweep with everything gated as decided: Modules tab disabled, cooling profiles grayed, dashboard read-only; 0 warnings/0 errors both targets; all tests green; fresh-machine smoke run.
Windows (MVP) — MIGRATED 2026-07-22 to a WiX v7 MSI (packaging/windows/subzeroframework.wxs, built
per-arch in the Windows matrix job, artifact "SubZero Framework - Windows {x64,arm64} Installer").
Originally implemented 2026-07-03 with Inno Setup (subzeroframework.iss, removed — recoverable from git
history); Inno 6.5+ became non-commercial-use-only without a paid license, while WiX v7's Open Source
Maintenance Fee applies only to organizations above USD $10k annual revenue, so this project uses WiX free
after a one-time wix eula accept wix7 (accepted 2026-07-22; CI re-accepts per run). Design:
- Installs the exact layout the app's helper discovery expects: UI at
INSTALLFOLDER(Program Files), packaged service atINSTALLFOLDER\service-package\windows\SubZeroFramework.Service.exe— soPackagedHelperAvailable=trueand the in-app lifecycle keeps working post-install. Verified by administrative extract: 967/967 payload files, both exes at the discovery paths. - Service registration is now declarative MSI (
ServiceInstall/ServiceControl+util:ServiceConfig): auto-start LocalSystemSubZeroFrameworkService, restart-on-failure ×3 @ 5 s (mirrors the old CLI'ssc failureconfig), stop-before-file-overwrite on upgrades, start after install, stop+deregister on uninstall — Windows Installer also quotes the ImagePath itself (closes the CWE-428 class). ServiceControl event mask verified = 163. Reinstall-over-the-top is a realMajorUpgrade(old product uninstalls first;AllowSameVersionUpgradesfor CI's 0.1. stamps). The in-app--service-managementCLI flow still works post-install — it re-points the same SCM entry. NOTE: machines that installed via the old Inno setup should uninstall it first; MSI upgrade logic only sees MSI products (only dev machines are affected — no public Inno release ever shipped). - The payload is harvested with a
<Files>glob; the two executables are excluded at staging time (robocopy /XFin CI, explicit deletes in the local driver) and authored explicitly, because the glob cannot exclude and both need addressable File ids (service registration, shortcut, launch checkbox). - MSIX was rejected for MVP: Win32 services in MSIX need restricted capabilities + mandatory signing; revisit only if package identity becomes necessary (it would also fix AppNotificationManager).
- Remaining: clean-machine validation (P0-1) and code signing — unsigned installers trip SmartScreen; decide cert vs. documented "More info → Run anyway" for the first releases.
Linux packaging — implemented 2026-07-03 in CI (packaging/linux/build-linux-packages.sh + the
package_arch_binary job): per-arch (x64 + arm64) .deb ×2 (UI, service w/ systemd enable-on-install),
.rpm ×2, combined tarball, and the AUR format (PKGBUILD + .SRCINFO + .install) plus binary
.pkg.tar.zst built via makepkg (aarch64 label via CARCH override — pure binary repack, no compilation).
Packages install to /usr/lib/subzeroframework/{ui,service} with the unit in /usr/lib/systemd/system/;
the in-app install flow should detect package-managed installs and defer (tracked above). First CI run is
the live validation of both pipelines.
Linux — yes, both .deb and an Arch package are practical, with two self-contained publishes (UI =
net10.0-desktop, service = worker). Tooling researched 2026-07-03:
- Service
.deb/.rpmviadotnet-releaser(chosen): its[service] publish = true+[service.systemd]config installs the app as a systemd service from the package, with a generated unit that already carriesRestart = always,RestartSec = 1,StartLimitBurst = 4/60s— i.e. the Linux twin of our Windowssc.exe failurerecovery config for free. Options:arguments,user+create_user(leaveuserunset ⇒ runs as root, which EC access requires), and raw unit overrides via[service.systemd.sections.Unit](e.g.After = "network.target"). Executable lands at/usr/local/bin/<app>. It also does deb + rpm + tar for linux-x64/arm64, changelog generation, and GitHub release publishing — it can become the whole Linux release driver.[msbuild] project = "SubZeroFramework.Service/SubZeroFramework.Service.csproj" [service] publish = true [service.systemd.sections.Unit] After = "network.target"
- UI
.deb: dotnet-releaser targets console apps and generates no.desktopentry, so package the Uno UI separately —dotnet-debor a trivialdpkg-debCI step (publish output +.desktop+ icon). Use a separate releaser/packaging config per app (a shared[service]would otherwise apply to every packable project). Prototype both packages in CI before committing to the split. - Arch: no dotnet tool emits
.pkg.tar.zst. Two viable paths, in order of preference:- AUR
-binPKGBUILD (~40 lines) whosesource=is the tar.gz that dotnet-releaser already publishes to the GitHub release — install under/usr/lib/subzeroframework/,/usr/binsymlink, unit into/usr/lib/systemd/system/,.desktop,options=(staticlibs). Optionally runmakepkgin a CI Arch container to attach a binary.pkg.tar.zstto releases. debtap(deb → pacman) as the "at worst" fallback — it works, and because our publishes are self-contained its main weakness (Debian→Arch dependency-name translation is heuristic, not 100%) barely applies. But even debtap's author recommends a real PKGBUILD, and converted packages are not acceptable for AUR distribution — so debtap is a user-side escape hatch to document, not our release pipeline.
- AUR
- Do not
PublishTrimmedthe Uno UI (reflection/XAML breakage risk); trimming the worker is optional. CONFIRMED EMPIRICALLY 2026-07-20 (applies to Windows too, not just Linux). The Windows publish profiles had been shippingPublishTrimmed=Truefor every non-Debug build since a stale Uno template default, so CI's installer was trimmed while every dev build (Debug forces it off) was not — the trimmed artifact had therefore never been run. It crashes on startup:0xc000027b(stowed WinRT exception) inMicrosoft.UI.Xaml.dll, HRESULT0x802B000A. The untrimmed publish of the same commit launches normally. Rooting the reflection-heavy assemblies first (UnitsNet, our own three, the navigation resolver, the toast COM activator) did not rescue it: the failure is inside WinUI's own CsWinRT marshalling layer — 35IL2081warnings acrossABI.System.Collections.Generic.*— and WindowsAppSDK is not trim-annotated. Trimming saved ~148 MB (574 → 425 MB) for a binary that does not start. Both pubxml files now setPublishTrimmed=Falseunconditionally;PublishReadyToRunis unrelated and stays ON. A full trim pass reports 52IL20xxwarnings, all inside dependencies — "zero trim warnings" is not reachable without abandoning Uno.Extensions. Note trim failures are lazy: a build can launch and still fail when a specific page or unit conversion is first touched, so re-enabling requires exercising every page, not a smoke test. The inert keep-descriptor block inSubZeroFramework.csprojdocuments where to start if this is ever revisited. - Prerequisite either way: P1 Linux systemd E2E validation (root EC access) must pass first.
- The curve worker (
FrameworkFanCurveControlWorker) drives stored curves server-side against live driving-sensor temperatures (interpolation identical to the client preview). Fixed 2026-07-18: the EC duty register only accepts whole percents andFrameworkEcConnection.SetFanDutythrows on fractional values, so every worker write of an interpolated (fractional) duty silently failed at Warning level — curves were reported active but never actuated.FrameworkDataProvider.SetFanDutyAsyncnow rounds at the single choke point before the EC write. - Per-fan CPU usage modifier / "CPU boost": REMOVED 2026-08-24 (user decision — a different approach
is planned). Taken out end to end: the
SetFanUsageModifierRPC,cpu_usage_modifier_strengthon both the request and the control state (proto field 21 is nowreserved),FanUsageModifierMath,FanUsageSmoothingFilter, the strength field onFanControlStateSnapshot/FanControlStateOptionsand its JSON persistence, the store'sSetCpuUsageModifier, the curve worker's smoothing + boost + stale-reading guards,IFrameworkFanControlClient.SetUsageModifierAsync, and theFanBoostSectionModelcard on the fan detail pane. A persistedCpuUsageModifierStrengthkey in an existingservice-settings.jsonis simply ignored on load — the options record no longer binds it — so no migration is needed. What the removed design was, for whoever writes the replacement: duty points added on top of the curve duty,strength × (e^(4·usage) − 1)/(e^4 − 1), fed by the primary tier's CPU reading with fast-attack / slow-decay smoothing (5 s half-life), rejecting hardware-info snapshots older than 10 s so a stalled poll decayed the boost instead of pinning it. Followers inherited the leader's already-boosted duty. It was applied where the curve interpolated, and it was flushed AFTER the mode/curve commit on Apply because the service rejected modifier writes while a preview hold was open. - GPU usage modifier: never implemented (user decision 2026-07-18). Hardware.Info has no GPU utilization; the GPU telemetry readers added since would now supply one if a replacement wants it.
- Custom-curve activation staging (2026-07-18, user-reported): switching a non-curve-driven fan into
Custom curve now stages immediately (
IsCustomActivationStaged) — pending pill + Preview/Revert light up with the loaded curve, no need to move a point first. Revert of a staged activation exits the editor back to the applied mode. - Adversarial review pass (2026-07-18) confirmed and fixed: (1) stale CPU readings — the worker now
rejects hardware-info snapshots that are unavailable or >10 s old, so a failed/stopped poll decays the
boost instead of freezing it; (2) the legacy
SetFanCustomCurvecommit path hand-built its persisted options and replaced the fan's whole config entry, wiping stored curve profiles, fan link, and modifier from disk (reachable today via Discard test) — it now persists viaBuildFanControlOptionslike every other command; (3)SetFanUsageModifierduring an open preview hold would have committed the volatile preview and disarmed the revert watchdog — it rejected with FailedPrecondition (FanPreviewWatchdog.HasOpenHold); that RPC is gone as of 2026-08-24, butHasOpenHoldremains and any new command that persists a fan's live state must make the same check; (4) read-modify-write races inFrameworkFanControlStateStore(e.g. workerRecordAppliedDutyvs a command) — all lookup→mutate→publish sequences now serialize under one lock; (5) the worker rounds target duty to the whole percent the EC takes before the 1% change threshold, so idle CPU jitter cannot cause write churn; (6) a sustained missing usage source with modifiers configured now logs a warning instead of being silently inert.
- Cooling profiles (flip
CoolingProfilesEnabled): preset semantics are implemented (Silent→Auto, Balanced→45%, Performance→65%, Turbo→Max, Custom derived); needs UX polish + decision on interaction with per-fan custom curves before enabling. - Modules page (re-enable tab): blocked on framework-dotnet FFI follow-ups recorded in the redesign plan §5 — report all 6 FW16 slots (only PD slots stream today), USB-topology→slot mapping, Microphone identity/privacy entry. Also confirm FW13 vs FW13 Pro routing assumption (IntelCoreUltra ⇒ Pro).
- FFI port capabilities (
Design Ideas/framework-port-capabilities.md): static USB-C port caps + non-PD port fixes in the Rust FFI (user rebuilds/publishes framework-dotnet). - Fan Control page visual pass to its PDF (functionally complete; redesign plan §9).
- Remaining integration tests (carried ⏳): status reconnect after service restart, long-lived stream startup/cancellation, telemetry contract parsing/history-window validation, broader startup/config binding coverage. CI note 2026-07-03: the pipeline now gates all publish/package jobs on the unit suite (Windows + Linux matrix) and runs a Windows startup smoke test (launch published app on a clean runner → window created → UI thread responsive; recovery page is the expected state without hardware). A real UI-automation suite (FlaUI is the right tool for WinUI 3; Uno.UITest targets WASM/mobile and WinAppDriver is unmaintained) stays post-MVP — runner flakiness + no Framework EC on runners limit its value versus on-device manual QA.
- framework-dotnet metadata versions — validated 2026-07-26: blocked on framework-dotnet; the SubZero
side is already complete.
SettingsAboutSectionModel.ResolveAssemblyMetadataalready reflects overAssemblyMetadataAttributeand degrades to the honest "Bundled with framework-dotnet" placeholder, so the two rows will light up with no SubZero code change the moment the library ships the keys. Confirmed by dumping every assembly-level attribute of the resolvedFrameworkDotnet 0.8.213: it carries exactly oneAssemblyMetadataAttribute, keyRepositoryUrl. There is also no fallback route — the nativeframework_lib_ffi.dllhas no VERSIONINFO resource and no version export. What remains is ~15 lines of MSBuild inframework-dotnet/framework-dotnet.csproj: a target (BeforeTargets="GetAssemblyAttributes", so the computed values exist before AssemblyInfo generation) that reads the twoCargo.tomlfiles the csproj already has paths to — the pack job checks outsubmodules: recursive, so both are on disk — and emits the attributes. Then bump the pin here. The values it would surface today areframework_lib_ffi0.6.2 andframework_lib0.6.4 (submodule atv0.6.4-3-g39f0f89, i.e. three commits past the tag, so considergit describefor precision). - Diagnostics UX: open-logs / copy-diagnostics actions; production logging guidance (Event Log / file) (carried ⏳).
- Fan safety hardening (carried ⏳/🟡): restore-to-auto from unhandled-termination paths, operator
policy when restore fails, multi-instance command coordination semantics, watchdog/heartbeat decision.
Added 2026-07-18 (review finding, pre-existing): any persisting command triggers a
reloadOnChangeconfig reload whoseApplyConfiguredStatesoverlay re-applies persisted state to all fans, which can clobber another fan's live volatile preview mid-session. Affects every persist path, not just the new modifier; fix direction: skip fans with an open preview hold during the overlay. - Linux service path (carried ⏳/🟡): systemd end-to-end validation (root/EC access), journald guidance, unit hardening options.
- Explicit Rx throttling policy for UI subscriptions where still missing (carried 🟡). Note the 2026-07-03 lesson: never subscribe long history windows or issue unary EC calls during startup-page VM construction — the Dashboard now renders live values only.
- NativeAOT for the service — investigated 2026-07-20: FEASIBLE, 0 hard blockers, deliberately
deferred past 0.1.0. The dependency stack is unusually AOT-ready:
Grpc.AspNetCore.Server2.80.0 andGrpc.Net.ClientshipIsTrimmablewith zeroRequires*annotations;Google.Protobuf3.35.1 guards dynamic-code paths behindRuntimeFeature.IsDynamicCodeSupported(we use generatedMessageParser, notJsonFormatter/descriptor reflection);Hardware.Info.Aothas zero references toSystem.Management, and its WmiLight dependency ships an explicitPublishAotstatic-link path; the EC FFI is 44Cdecl/ExactSpellingP/Invokes over 58 blittable structs with no string/array/delegate marshalling; and the service source contains noActivator/Type.GetType/MakeGenericType/JsonSerializer/Expressionat all. The reason to defer is one SILENT regression, not a build failure.PublishAotauto-enables the configuration-binding source generator, which does not bindinit-only properties on records with only a parameterless constructor (CanSet = publicSetter && !isInitOnly; theSetOnInitpath applies only to parameterized constructors). Every property ofFrameworkServiceOptionsandFanControlStateOptionsis{ get; init; }, andAddOptions<T>().Bind()binds into an already-constructed instance. Under AOT, with no diagnostic and no exception:PollingIntervalreverts from appsettings'00:00:02to the 150 ms code default — 13× the validated EC polling rate, on every machine, silently;AllowFanControlCommandspinsfalse, so the persisted overlay can never re-enable fan control;FanControlStatesstays empty, so persisted curves/profiles never restore. Prerequisites if revived (in order): (1)init→seton both records — a no-op under CoreCLR, since reflection ignores theinitmodreq; (2) replaceLoggerProviderOptions.RegisterProviderOptions(Program.cs:49,[RequiresDynamicCode], not interceptable — the bind lives inConfigureFromConfigurationOptions<T>in a precompiled assembly) — do NOT just delete it, or the Event Log source silently changes; (3) shipSystem.Diagnostics.EventLog.Messages.dllexplicitly (ILC drops it as a culture-neutral resource assembly, giving "description … cannot be found" in Event Viewer); (4) provision a native toolchain in CI — no C++ tools are installed today, andwindows-11-armneeds the ARM64 VC component specifically, Linux needsclang+objcopy; (5) mind the Linux glibc floor — AOT'sScrt1.opulls__libc_start_main@GLIBC_2.34, raising the floor from today's 2.27 and silently dropping RHEL 8 / Ubuntu 20.04 / Debian 11, which no package format currently declares. CI can prove it compiles, links, starts, and binds options; CI cannot prove EC reads/writes, fan duty actuation, or — critically — that fan restore-to-auto still completes inside theAppDomain.ProcessExitbackstop before an AOT teardown. That last one is hardware-safety-critical and needs a physical soak, which is precisely why this waits until after the first release.
Service-owned display-unit preferences (— reversed 2026-07-03: units are client-only now; the backend user-preferences vertical was removed.FrameworkUserPreferencesService)Relocate— obsolete with client-only units. Relocatinguser-preferences.jsonvia serviceservice-settings.jsonremains possible over the existing gRPC surface but has no UI in the redesigned Settings; treat as low-priority backlog, single shared relocate flow if ever revived.- Dashboard quick-toggles card, per-fan duty stepper/boost on Dashboard — removed by design decision 2026-07-03 (control lives on Fan Curve Profiles; Dashboard is read-only).