Der Akku bekommt ein Gedächtnis

Verlauf über 24 Stunden, sieben Tage und dreißig Tage, aufgezeichnet alle fünf
Minuten. Als Säulen und nicht als Linie: eine Linie verbindet auch dort, wo
nichts gemessen wurde, und ein zugeklappter Rechner hatte keinen Ladestand —
eine Linie darüber behauptet einen.

Dazu Segelmodus und Hitzeschutz als reine Regeln im Helfer. Beide haben zwei
Schwellen, eine zum Anhalten und eine zum Weitermachen. Ohne diesen Abstand
hinge der Akku am Limit und würde bei jedem verlorenen Prozent nachgeladen —
hunderte winzige Ladevorgänge am Tag, und genau die altern ihn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-05 21:06:06 +02:00
co-authored by Claude Opus 5
parent edb327cabe
commit f289d02c37
9 changed files with 1357 additions and 690 deletions
@@ -82,14 +82,14 @@ private struct CompactMetric: View {
private var mode: MetricDisplayMode { model.displayMode(for: metric) }
var body: some View {
VStack(alignment: .leading, spacing: 4) {
VStack(alignment: .leading, spacing: Onyx.Space.xs) {
Label {
Text(String(localized: .init(metric.localizationKey), bundle: .module))
.font(Onyx.Font.caption)
.foregroundStyle(Onyx.Color.textSecondary)
} icon: {
Image(systemName: metric.symbolName)
.font(.system(size: 11))
.font(.system(size: Onyx.Symbol.small))
.foregroundStyle(Onyx.Color.accent)
}
@@ -103,7 +103,7 @@ private struct CompactMetric: View {
if let detail = MetricSummary.detail(metric, model.snapshot) {
Text(detail)
.font(.system(size: 9))
.font(Onyx.Font.micro)
.monospacedDigit()
.foregroundStyle(Onyx.Color.textTertiary)
.lineLimit(1)
@@ -121,7 +121,7 @@ private struct WideMetric: View {
// Übereinander, nicht nebeneinander: die Kachel ist hochkant, und ein
// Verlauf über die volle Breite liest sich besser als einer, der sich
// die Zeile mit der Zahl teilt.
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: Onyx.Space.md) {
CompactMetric(model: model, metric: metric)
Spacer(minLength: 0)
Sparkline(values: model.series(metric),
@@ -200,11 +200,11 @@ private struct SensorList: View {
let detailed: Bool
var body: some View {
VStack(alignment: .leading, spacing: 3) {
VStack(alignment: .leading, spacing: Onyx.Space.xs) {
ForEach(snapshot.sensors.prefix(detailed ? 8 : 3)) { sensor in
HStack(spacing: 6) {
HStack(spacing: Onyx.Space.sm) {
Text(sensor.name)
.font(.system(size: 10))
.font(Onyx.Font.caption)
.foregroundStyle(Onyx.Color.textSecondary)
.lineLimit(1)
Spacer(minLength: 0)
@@ -215,17 +215,17 @@ private struct SensorList: View {
}
if detailed, !snapshot.fans.isEmpty {
Divider().overlay(Onyx.Color.hairline).padding(.vertical, 2)
OnyxHairline().padding(.vertical, Onyx.Space.xxs)
ForEach(snapshot.fans) { fan in
HStack(spacing: 6) {
HStack(spacing: Onyx.Space.sm) {
Image(systemName: "fan")
.font(.system(size: 9))
.font(.system(size: Onyx.Symbol.caption))
.foregroundStyle(Onyx.Color.textTertiary)
Text("\(Int(fan.rpm))")
.font(Onyx.Font.metricSmall)
.foregroundStyle(Onyx.Color.textSecondary)
Text(verbatim: "U/min")
.font(.system(size: 9))
.font(Onyx.Font.micro)
.foregroundStyle(Onyx.Color.textTertiary)
Spacer(minLength: 0)
}
@@ -351,8 +351,10 @@ private struct BatteryTile: View {
var body: some View {
let battery = model.snapshot.battery
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .firstTextBaseline, spacing: 7) {
VStack(alignment: .leading, spacing: Onyx.Space.md) {
// Der Verlauf steht unter den Zahlen, nicht über ihnen: die Frage
// „wie voll ist er jetzt" kommt vor „wie war es gestern".
HStack(alignment: .firstTextBaseline, spacing: Onyx.Space.md) {
// Das Symbol folgt dem Ladestand — sonst zeigt es bei 5 %
// dasselbe wie bei 95 % und trägt nichts bei.
if let battery {
@@ -362,7 +364,7 @@ private struct BatteryTile: View {
.foregroundStyle(tint(battery))
} else {
Image(systemName: BatterySymbol.unavailable)
.font(.system(size: 13))
.font(.system(size: Onyx.Symbol.medium))
.foregroundStyle(Onyx.Color.textTertiary)
}
Text(MetricFormat.percent(battery?.charge ?? 0))
@@ -386,7 +388,7 @@ private struct BatteryTile: View {
Spacer(minLength: 0)
VStack(alignment: .leading, spacing: 3) {
VStack(alignment: .leading, spacing: Onyx.Space.xs) {
if let watts = battery.watts, abs(watts) > 0.1 {
row(watts > 0 ? "battery.charging" : "battery.draining",
value: String(format: "%.1f W", abs(watts)))
@@ -405,19 +407,23 @@ private struct BatteryTile: View {
// Der Energiesparmodus erklärt, warum die Maschine langsamer
// wirkt. Ohne diesen Hinweis sucht man den Grund woanders.
if battery.isLowPower {
HStack(spacing: 4) {
Image(systemName: "leaf.fill").font(.system(size: 9))
HStack(spacing: Onyx.Space.xs) {
Image(systemName: "leaf.fill").font(.system(size: Onyx.Symbol.caption))
Text("battery.lowPower", bundle: .module).font(Onyx.Font.metricSmall)
}
.foregroundStyle(Onyx.Color.warning)
}
// Der Verlauf steht unten: „wie voll ist er jetzt" kommt vor
// „wie war es gestern".
BatteryHistoryView(store: model.batteryHistory)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
private func row(_ key: String.LocalizationValue, value: String) -> some View {
HStack(spacing: 4) {
HStack(spacing: Onyx.Space.xs) {
Text(String(localized: key, bundle: .module))
.foregroundStyle(Onyx.Color.textTertiary)
Spacer(minLength: 4)