fix: house plan editor script scoping, single announcement of switch state, ADR note
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d9a03dcd5
commit
0aa0947727
6 files changed
+26
-17
No files matched your search
@@ -72,6 +72,8 @@ führt Befehle aus und schreibt das Aktionsprotokoll. Die Passung von Befehl und
|
||||
Die Haus-Seite lädt `overview` (schnell, nur Datenbank) und danach `states` (Geräteabfragen) und fragt alle 10 s erneut. Ein nicht erreichbares Gerät liefert `Online = false` mit Meldung, nie einen Fehler für die ganze Anfrage.
|
||||
SignalR-Live-Updates sind nicht nötig: Geräte werden auch außerhalb der App geschaltet, daher bliebe Abfragen ohnehin nötig.
|
||||
|
||||
Hinweis: Batteriesensoren (z. B. Shelly H&T) schlafen zwischen den Meldungen und erscheinen bei Abfrage ggf. dauerhaft als „offline“; sie eignen sich nur mit Netzbetrieb oder einem späteren Push-/MQTT-Treiber.
|
||||
|
||||
## Folgen
|
||||
* Neue Tabellen (Migration `Phase17Home`), neue Permissions mit Matrix-Tests je Zelle, neue Audit-Aktionen.
|
||||
* Der Server braucht Netzwerkzugang zu den Geräten (siehe Punkt 3); ohne Echtgerät sind Seed und AppHost unverändert, Tests nutzen `FakeShellyServer`.
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<MudButton Variant="@(isOn ? Variant.Filled : Variant.Outlined)" Color="@(isOn ? Color.Success : Color.Default)" Class="fp-device-toggle"
|
||||
StartIcon="@Icons.Material.Filled.PowerSettingsNew" Disabled="@(!CanOperate)"
|
||||
OnClick="() => OnCommand.InvokeAsync(isOn ? HomeCommand.TurnOff : HomeCommand.TurnOn)"
|
||||
aria-pressed="@(isOn ? "true" : "false")" aria-label="@($"{Device.Name}: {(isOn ? "an" : "aus")}. Zum {(isOn ? "Ausschalten" : "Einschalten")} drücken")">
|
||||
aria-pressed="@(isOn ? "true" : "false")" aria-label="@Device.Name">
|
||||
@(isOn ? "An" : "Aus")
|
||||
</MudButton>
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
class="@($"fp-pin fp-pin-{HouseUi.CssClass(status)}{(selected ? " fp-pin-selected" : string.Empty)}")"
|
||||
style="@PinStyle(pin)"
|
||||
aria-label="@HouseUi.PinLabel(device, state)"
|
||||
aria-pressed="@(HouseUi.IsToggleable(device) ? (state?.IsOn is true ? "true" : "false") : null)"
|
||||
aria-pressed="@(HouseUi.IsPressable(device, state) ? (state?.IsOn is true ? "true" : "false") : null)"
|
||||
title="@($"{device.Name}: {HouseUi.StatusText(device, state)}")"
|
||||
@onclick="() => OnPinClick.InvokeAsync(device)">
|
||||
<MudIcon Icon="@HouseUi.Icon(device.Icon, device.Kind)" Size="Size.Small" aria-hidden="true" />
|
||||
|
||||
@@ -147,12 +147,16 @@ public static class HouseUi
|
||||
public static bool IsToggleable(HomeDeviceDto device) =>
|
||||
device.CanControl && HomeRules.IsCommandAllowed(device.Kind, HomeCommand.Toggle);
|
||||
|
||||
/// <summary>Beschriftung des Plan-Symbols für Screenreader, inklusive Aktion.</summary>
|
||||
public static string PinLabel(HomeDeviceDto device, HomeDeviceStateDto? state)
|
||||
{
|
||||
var text = $"{device.Name}, {StatusText(device, state)}";
|
||||
return IsToggleable(device) ? $"{text}. Zum Umschalten antippen." : $"{text}.";
|
||||
}
|
||||
/// <summary>
|
||||
/// Zugänglicher Name des Plan-Symbols. Schaltbare, erreichbare Geräte tragen nur den Namen – der Zustand steht in <c>aria-pressed</c>
|
||||
/// (sonst würde er doppelt vorgelesen); alle anderen nennen den Status im Namen.
|
||||
/// </summary>
|
||||
public static string PinLabel(HomeDeviceDto device, HomeDeviceStateDto? state) =>
|
||||
IsPressable(device, state) ? device.Name : $"{device.Name}, {StatusText(device, state)}.";
|
||||
|
||||
/// <summary>Schalter-Zustand per <c>aria-pressed</c> ausdrücken (nur wenn schaltbar und erreichbar).</summary>
|
||||
public static bool IsPressable(HomeDeviceDto device, HomeDeviceStateDto? state) =>
|
||||
IsToggleable(device) && state is { Online: true, IsOn: not null };
|
||||
|
||||
public static string FormatKind(HomeDeviceKind kind) => HomeRules.DisplayName(kind);
|
||||
|
||||
|
||||
@@ -4,13 +4,20 @@
|
||||
const clamp = value => Math.min(1, Math.max(0, value));
|
||||
const arrows = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
|
||||
|
||||
const detach = container => {
|
||||
if (container && container._fpHome) {
|
||||
container._fpHome.abort();
|
||||
delete container._fpHome;
|
||||
}
|
||||
};
|
||||
|
||||
window.fpHome = {
|
||||
attach(container, reference) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.detach(container);
|
||||
detach(container);
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
container._fpHome = controller;
|
||||
@@ -83,11 +90,6 @@
|
||||
}, { signal });
|
||||
},
|
||||
|
||||
detach(container) {
|
||||
if (container && container._fpHome) {
|
||||
container._fpHome.abort();
|
||||
delete container._fpHome;
|
||||
}
|
||||
},
|
||||
detach,
|
||||
};
|
||||
})();
|
||||
@@ -241,9 +241,10 @@ public sealed class HousePageTests : BunitContext
|
||||
Assert.Equal("left:25%;top:50%", pins[0].GetAttribute("style"));
|
||||
Assert.Equal("left:80%;top:10%", pins[1].GetAttribute("style"));
|
||||
Assert.All(pins, pin => Assert.Equal("BUTTON", pin.TagName));
|
||||
Assert.Contains("Wohnzimmer-Licht, Aus. Zum Umschalten antippen.", pins[0].GetAttribute("aria-label"));
|
||||
Assert.Equal("Wohnzimmer-Licht", pins[0].GetAttribute("aria-label")); // Zustand nur über aria-pressed
|
||||
Assert.Equal("false", pins[0].GetAttribute("aria-pressed"));
|
||||
Assert.Contains("Rollo, Offen.", pins[1].GetAttribute("aria-label"));
|
||||
Assert.DoesNotContain("Umschalten", pins[1].GetAttribute("aria-label")); // Rollo: kein Klick-Umschalten
|
||||
Assert.Null(pins[1].GetAttribute("aria-pressed")); // Rollo: kein Klick-Umschalten
|
||||
Assert.Equal("Aus", pins[0].QuerySelector(".fp-pin-badge")!.TextContent); // Kurztext zusätzlich zur Farbe
|
||||
Assert.Contains("fp-pin-off", pins[0].ClassName);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user