Merge Home & Phone into a three-tab Home that knows your house

Home is now Overview | My Home | Phone. Overview leads with quick-action
tiles (focus, Do Not Disturb, health, snapshots, storage), keeps the
findings card — updates fold in, the reclaim-space prompt is gone on
purpose — and adds glance cards, the next calendar event, and weather.

My Home groups every light by Home Assistant area: the helper gained an
`areas` command (one REST template render, no websocket), and the rooms
degrade to a flat list on setups without areas. The favorites editor and
connection card moved intact. Phone gains a vitals strip — battery and
cell signal read from KDE Connect's plugin D-Bus objects, where absence
is data, not an error — beside ring, clipboard, send-a-file, and the
BlueBubbles handoff.

The retired home-phone id resolves to my-home forever via a new alias
map in SettingsRoutes (with a hasOwnProperty guard so prototype names
cannot leak into settingsPage). Storage no longer claims 0 B free — the
old page read a field the disks helper never emitted.

Contracts updated alongside; per the new workflow, the full suite runs
once at the end of the redesign (see the test backlog note).

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-23 22:06:18 -04:00
parent 5490fd285d
commit 7578348db1
40 changed files with 2578 additions and 703 deletions
+46 -3
View File
@@ -43,6 +43,12 @@ Singleton {
readonly property int pairedCount: root.devices.filter(device => device.paired).length
readonly property var phoneActions: root.preferredPhone?.actions ?? []
// Vitals are null whenever the phone is away or the plugin is off, which is
// the normal state rather than an error -- consumers show nothing, not a
// zero. `?? null` keeps `undefined` from leaking out as a distinct case.
readonly property var phoneBattery: root.preferredPhone?.battery ?? null
readonly property var phoneSignal: root.preferredPhone?.signal ?? null
function supports(action: string): bool {
return root.phoneActions.indexOf(action) >= 0;
}
@@ -53,13 +59,44 @@ Singleton {
statusProc.running = true;
}
// The helper emits `battery` and `signal` as either null or a complete
// object. These rebuild them anyway so a partial payload -- an old helper,
// a field that arrived as a string -- becomes null rather than a card
// rendering "undefined%".
function normalizeBattery(value: var): var {
if (!value || typeof value.charge !== "number" || value.charge < 0)
return null;
return {
charge: Math.round(value.charge),
charging: value.charging === true
};
}
function normalizeSignal(value: var): var {
if (!value || typeof value.strength !== "number" || value.strength < 0)
return null;
return {
networkType: String(value.networkType ?? ""),
strength: Math.round(value.strength)
};
}
function normalizeDevice(device: var): var {
return Object.assign({}, device, {
battery: root.normalizeBattery(device.battery),
signal: root.normalizeSignal(device.signal)
});
}
function consumeStatus(text: string): void {
if (root.fixtureMode)
return;
try {
const result = JSON.parse(text);
root.available = result.available === true;
root.devices = Array.isArray(result.devices) ? result.devices : [];
root.devices = Array.isArray(result.devices)
? result.devices.map(device => root.normalizeDevice(device))
: [];
root.lastError = String(result.error ?? "");
} catch (error) {
root.available = false;
@@ -188,13 +225,19 @@ Singleton {
root.available = true;
root.lastError = "";
root.recentExchange = null;
// Vitals track reachability the way the helper's do: the plugin objects
// only exist for a phone that is actually there, so the offline fixture
// carries nulls and the reachable ones carry readable values.
const reachable = name !== "offline";
root.devices = [{
id: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
name: "Fixture iPhone",
type: "phone",
paired: true,
reachable: name !== "offline",
actions: ["clipboard", "ping", "ring", "share"]
reachable: reachable,
actions: ["clipboard", "ping", "ring", "share"],
battery: reachable ? { charge: 82, charging: true } : null,
signal: reachable ? { networkType: "LTE", strength: 3 } : null
}];
root.transferActive = name === "transfer";
root.transferFileName = name === "transfer" ? "Fixture document.pdf" : "";