Files
Panama/docs/superpowers/plans/2026-08-17-panama-control-center.md
T

658 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Panama Control Center Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Expand Panama Quick Settings into the approved Control Center with secure Home Assistant favourites and capability-aware iPhone continuity.
**Architecture:** Two executable Python helpers own the external boundaries: KDE Connect CLI/D-Bus and the Home Assistant REST API/credential lookup. Quickshell singletons normalize those JSON contracts for focused Home and Phone components inside the existing Quick Settings window; Ongoing and Signal Glass consume transfer lifecycle without duplicating state.
**Tech Stack:** Python 3.14 standard library, Secret Service CLI, KDE Connect 25.12 CLI/D-Bus, Home Assistant REST API, Quickshell 0.3/QML, Bash contract tests, Python `unittest`, jq, Hyprland 0.56
**Spec:** `docs/superpowers/specs/2026-08-17-panama-control-center-design.md`
## Global Constraints
- Keep Wi-Fi, Bluetooth, Focus, Caffeine, Night Light, audio, account, settings, power, Escape, and outside-click behavior intact.
- The Control Center width is 430 logical pixels and its top margin is exactly `Theme.barHeight + 2`.
- Only one of Wi-Fi, Bluetooth, sink, source, Home, and Phone may be expanded at once.
- Never print Home Assistant tokens, Authorization headers, clipboard text, certificate fingerprints, IP addresses, response bodies, or full local file paths.
- Home Assistant tokens remain in the gitignored environment file or Secret Service; never write one into tracked content.
- Home Assistant toggle commands accept only entity IDs from the configured favourites snapshot.
- KDE Connect actions derive from advertised plugins; no fake battery or remote-input control is allowed.
- Every Home toggle, file chooser, file send, clipboard send, and Ring action requires a user click.
- Home toggle success, phone Ring, and clipboard success remain globally quiet.
- A running file send appears in Ongoing without a fabricated percentage.
- Automated tests do not toggle a real Home entity, ring the phone, send clipboard contents, or transfer a real file.
- No pulse, shimmer, animated gradient, idle animation, or high-frequency polling.
---
## File map
### KDE Connect boundary
- `config/dot/quickshell/scripts/panama-kdeconnect` — validates device/file inputs, normalizes device/plugin status, and invokes allow-listed actions.
- `tests/quickshell/kdeconnect_bridge_test.py` — pure parser, capability, validation, and command tests with fake runners.
- `tests/quickshell/kdeconnect-helper-contract.sh` — live read-only status contract with names and IDs redacted.
- `config/dot/quickshell/services/KdeConnect.qml` — singleton device, action, transfer, fixture, and recent-exchange state.
### Home Assistant boundary
- `config/dot/quickshell/scripts/panama-home-assistant` — configuration resolution, normalized REST reads, allow-listed toggles, and URL handoff.
- `tests/quickshell/home_assistant_bridge_test.py` — local fake API and credential/configuration precedence tests.
- `tests/quickshell/home-assistant-helper-contract.sh` — live read-only probe/snapshot contract with entity content redacted.
- `config/dot/quickshell/services/HomeAssistant.qml` — singleton entities, stale state, per-entity action state, fixture, and inline errors.
### Control Center presentation and integration
- `config/dot/quickshell/config/Theme.qml` — dedicated `controlCenterWidth` and `controlCenterTopGap` geometry tokens.
- `config/dot/quickshell/modules/quicksettings/QuickSettings.qml` — tighter top attachment and service preparation.
- `config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml` — expanded composition and exclusive section routing.
- `config/dot/quickshell/modules/quicksettings/ControlSectionHeader.qml` — quiet section label and optional trailing action.
- `config/dot/quickshell/modules/quicksettings/HomeControls.qml` — four favourites, expanded configured list, loading/stale/failure states.
- `config/dot/quickshell/modules/quicksettings/HomeTile.qml` — one domain-aware favourite tile.
- `config/dot/quickshell/modules/quicksettings/PhoneControls.qml` — approved compact Continuity Card.
- `config/dot/quickshell/modules/quicksettings/RecentExchange.qml` — one session-local completed transfer row.
- `config/dot/quickshell/modules/bar/StatusCluster.qml` — reachable-phone glyph inside the existing click target.
- `config/dot/quickshell/services/DeviceEvents.qml` — consume shared KDE Connect state instead of a second polling process.
- `config/dot/quickshell/services/Ongoing.qml` — append and control one running phone transfer.
- `config/dot/quickshell/services/StatusEvents.qml` — open a completed received/sent path through the existing allow-listed action.
- `config/dot/quickshell/shell.qml` — diagnostics/fixture IPC for the two services.
- `tests/quickshell/control-center-services-contract.sh` — deterministic service, activity, and event contract.
- `tests/quickshell/control-center-contract.sh` — panel geometry, routing, components, and live layer contract.
### Documentation
- `config/dot/hypr/DESKTOP-PARITY.md` — mark both remaining integrations live.
- `config/dot/hypr/README.md` — document Control Center, credential resolution, and iOS capability limits.
---
### Task 1: KDE Connect helper contract
**Files:**
- Create: `config/dot/quickshell/scripts/panama-kdeconnect`
- Create: `tests/quickshell/kdeconnect_bridge_test.py`
- Create: `tests/quickshell/kdeconnect-helper-contract.sh`
**Interfaces:**
- Produces: `panama-kdeconnect status` JSON with `available` and `devices`.
- Produces device fields: `id`, `name`, `type`, `paired`, `reachable`, and `actions`.
- Produces: `send-file DEVICE FILE`, `send-clipboard DEVICE`, and `ring DEVICE` with one normalized JSON result.
- Accepts an injectable `runner: Callable[[list[str]], CompletedProcess[str]]` in pure functions.
- [ ] **Step 1: Write failing parser and action tests**
Create `tests/quickshell/kdeconnect_bridge_test.py` with import-by-path and these cases:
```python
class KdeConnectBridgeTest(unittest.TestCase):
def test_status_omits_network_and_exposes_supported_ios_actions(self):
listing = "- Fixture iPhone: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB on 192.0.2.7 via LAN (paired and reachable)\n1 device found\n"
plugins = ["kdeconnect_clipboard", "kdeconnect_findmyphone", "kdeconnect_ping", "kdeconnect_share"]
device = bridge.normalize_device_line(listing.splitlines()[0], plugins)
self.assertEqual(device["name"], "Fixture iPhone")
self.assertEqual(device["actions"], ["clipboard", "ping", "ring", "share"])
self.assertNotIn("192.0.2.7", json.dumps(device))
def test_invalid_device_id_is_rejected(self):
with self.assertRaisesRegex(ValueError, "invalid-device"):
bridge.validate_device_id("phone; shutdown")
def test_send_file_requires_a_regular_file(self):
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(ValueError, "invalid-file"):
bridge.validate_file(pathlib.Path(directory))
def test_action_command_uses_separate_arguments(self):
command = bridge.action_command("ring", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
self.assertEqual(command, ["kdeconnect-cli", "-d", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "--ring"])
```
- [ ] **Step 2: Run the test and verify RED**
Run `python3 tests/quickshell/kdeconnect_bridge_test.py`.
Expected: FAIL because `panama-kdeconnect` does not exist.
- [ ] **Step 3: Implement the minimal helper**
Implement public functions named `validate_device_id`, `validate_file`,
`normalize_device_line`, `action_command`, `collect_status`, `invoke_action`,
and `main`. Use these exact validation and command-building implementations:
```python
DEVICE_ID = re.compile(r"^[A-Fa-f0-9]{32,64}$")
PLUGIN_ACTIONS = {
"kdeconnect_clipboard": "clipboard",
"kdeconnect_findmyphone": "ring",
"kdeconnect_ping": "ping",
"kdeconnect_share": "share",
}
def validate_device_id(value: str) -> str:
if not DEVICE_ID.fullmatch(value):
raise ValueError("invalid-device")
return value
def validate_file(value: pathlib.Path) -> pathlib.Path:
resolved = value.expanduser().resolve(strict=True)
if not resolved.is_file():
raise ValueError("invalid-file")
return resolved
def action_command(action: str, device_id: str, file_path: pathlib.Path | None = None) -> list[str]:
device_id = validate_device_id(device_id)
options = {
"ring": ["--ring"],
"clipboard": ["--send-clipboard"],
}
if action == "share" and file_path is not None:
return ["kdeconnect-cli", "-d", device_id, "--share", str(validate_file(file_path))]
if action not in options:
raise ValueError("unsupported-action")
return ["kdeconnect-cli", "-d", device_id, *options[action]]
```
`normalize_device_line(line: str, plugins: list[str]) -> dict[str, object]`
must parse one detailed CLI line and omit transport/address text.
`collect_status(runner=subprocess.run) -> dict[str, object]` must return
`available` plus normalized devices. `invoke_action -> dict[str, object]`
must run only `action_command` output. `main(argv: list[str]) -> int` must emit
one compact JSON object and map validation failures to the public error code.
Call `kdeconnect-cli --list-devices` without a shell. For each parsed device,
call `busctl --user call org.kde.kdeconnect /modules/kdeconnect/devices/DEVICE
org.kde.kdeconnect.device loadedPlugins`; parse only plugin names. Return generic
error codes `unavailable`, `device-offline`, `unsupported-action`,
`invalid-device`, `invalid-file`, or `action-failed`, never raw stderr.
For actions, construct exactly one of:
```python
["kdeconnect-cli", "-d", device_id, "--ring"]
["kdeconnect-cli", "-d", device_id, "--send-clipboard"]
["kdeconnect-cli", "-d", device_id, "--share", str(file_path)]
```
Make the helper executable.
- [ ] **Step 4: Run unit tests and verify GREEN**
Run `python3 tests/quickshell/kdeconnect_bridge_test.py`.
Expected: all cases PASS with no device name, address, or file path printed by the test runner.
- [ ] **Step 5: Add and run the redacted live contract**
Create `tests/quickshell/kdeconnect-helper-contract.sh` that runs `status` and asserts:
```bash
jq -e '
.available == true and
(.devices | type == "array" and length >= 1) and
([.devices[] | has("id") and has("name") and has("type") and has("paired") and has("reachable") and has("actions")] | all) and
([.devices[].actions[] | IN("clipboard", "ping", "ring", "share")] | all)
' <<<"$status" >/dev/null
```
Print only `PASS (N paired, N reachable; identities redacted)`.
- [ ] **Step 6: Commit Task 1**
```bash
git add config/dot/quickshell/scripts/panama-kdeconnect \
tests/quickshell/kdeconnect_bridge_test.py \
tests/quickshell/kdeconnect-helper-contract.sh
git commit -m "Add the Panama KDE Connect bridge"
```
---
### Task 2: Home Assistant helper contract
**Files:**
- Create: `config/dot/quickshell/scripts/panama-home-assistant`
- Create: `tests/quickshell/home_assistant_bridge_test.py`
- Create: `tests/quickshell/home-assistant-helper-contract.sh`
**Interfaces:**
- Produces: `probe`, `snapshot`, `toggle ENTITY_ID`, and `open`.
- Produces snapshot fields: `ok`, `configured`, `generatedAt`, `entities`, and `error`.
- Produces entity fields: `id`, `name`, `domain`, `state`, `available`, and `active`.
- Configuration precedence: `PANAMA_HOME_ASSISTANT_*`, GNOME extension dconf, then Secret Service for the token.
- [ ] **Step 1: Write failing configuration, normalization, and fake-API tests**
Create `tests/quickshell/home_assistant_bridge_test.py` with a local
`ThreadingHTTPServer`. Cover:
```python
def test_environment_configuration_wins_over_legacy_sources(self):
env = {
"PANAMA_HOME_ASSISTANT_URL": "https://home.example",
"PANAMA_HOME_ASSISTANT_TOKEN": "fixture-token",
"PANAMA_HOME_ASSISTANT_ENTITIES": "light.kitchen,light.hall",
}
config = bridge.resolve_config(env, legacy=lambda: (_ for _ in ()).throw(AssertionError()))
self.assertEqual(config.entity_ids, ("light.kitchen", "light.hall"))
def test_snapshot_filters_and_preserves_configured_order(self):
raw = [
{"entity_id": "light.hall", "state": "off", "attributes": {"friendly_name": "Hall"}},
{"entity_id": "sensor.private", "state": "1", "attributes": {}},
{"entity_id": "light.kitchen", "state": "on", "attributes": {"friendly_name": "Kitchen"}},
]
result = bridge.normalize_entities(raw, ("light.kitchen", "light.hall"))
self.assertEqual([item["name"] for item in result], ["Kitchen", "Hall"])
self.assertNotIn("sensor.private", json.dumps(result))
def test_toggle_rejects_unconfigured_entity(self):
with self.assertRaisesRegex(ValueError, "entity-not-configured"):
bridge.ensure_configured("light.office", ("light.kitchen",))
def test_authentication_error_is_redacted(self):
self.assertEqual(bridge.public_http_error(401, "sensitive response"), "authentication-required")
```
The fake server must also assert the request uses
`Authorization: Bearer fixture-token`, that snapshot calls `GET /api/states`,
and that toggle calls `POST /api/services/homeassistant/toggle` with only
`{"entity_id":"light.kitchen"}`. Do not print the header.
- [ ] **Step 2: Run the tests and verify RED**
Run `python3 tests/quickshell/home_assistant_bridge_test.py`.
Expected: FAIL because `panama-home-assistant` does not exist.
- [ ] **Step 3: Implement configuration and REST boundaries**
Implement the exact `Config` type and these public functions:
```python
@dataclass(frozen=True)
class Config:
base_url: str
token: str
entity_ids: Sequence[str]
def ensure_configured(entity_id: str, configured: Sequence[str]) -> str:
if entity_id not in configured:
raise ValueError("entity-not-configured")
return entity_id
def public_http_error(status: int, body: str) -> str:
del body
return "authentication-required" if status in {401, 403} else "request-failed"
def normalize_entities(raw: list[dict[str, object]], configured: Sequence[str]) -> list[dict[str, object]]:
by_id = {str(item.get("entity_id", "")): item for item in raw}
result = []
for entity_id in configured:
item = by_id.get(entity_id)
if not item or not isinstance(item.get("attributes"), dict):
continue
state = str(item.get("state", "unavailable"))
available = state not in {"unknown", "unavailable"}
result.append({
"id": entity_id,
"name": str(item["attributes"].get("friendly_name") or entity_id.split(".", 1)[1].replace("_", " ").title()),
"domain": entity_id.split(".", 1)[0],
"state": state,
"available": available,
"active": available and state not in {"off", "closed", "idle", "standby"},
})
return result
```
`resolve_config(env, legacy=load_legacy_config) -> Config` implements the
documented precedence. `load_legacy_config(runner=subprocess.run) -> Config`
reads only the named dconf and Secret Service values. `request_json`
performs one authorized request. `collect_snapshot(config) -> dict` calls
`GET /api/states`; `toggle(config, entity_id) -> dict` validates then calls the
Home Assistant toggle service; `main(argv: list[str]) -> int` emits compact JSON.
Read `config/bash/env` only through a non-interactive Bash subprocess that emits
the three named Panama values and nothing else. The legacy path reads only
`hass-url`, `hass-enabled-entities`, and `secret-tool lookup token_string
user_token`. Validate the base URL as HTTP(S), strip a trailing slash, reject
entity IDs outside `^[a-z_]+\.[a-z0-9_]+$`, deduplicate while preserving order,
and use `urllib.request` with a 5-second timeout.
Map state as follows:
```python
available = state not in {"unknown", "unavailable"}
active = available and state not in {"off", "closed", "idle", "standby"}
```
Return only normalized entities. Map 401/403 to `authentication-required`,
timeouts and transport errors to `unreachable`, and malformed JSON to
`invalid-response`.
- [ ] **Step 4: Run unit tests and verify GREEN**
Run `python3 tests/quickshell/home_assistant_bridge_test.py`.
Expected: all tests PASS and the fake server records no unexpected route.
- [ ] **Step 5: Add and run the redacted live contract**
The contract runs `probe` and `snapshot`, validates the schema, and prints only
`PASS (configured=true, N favourites; contents redacted)`. It must pipe JSON to
`jq` without printing friendly names, entity IDs, URL, or raw errors.
- [ ] **Step 6: Commit Task 2**
```bash
git add config/dot/quickshell/scripts/panama-home-assistant \
tests/quickshell/home_assistant_bridge_test.py \
tests/quickshell/home-assistant-helper-contract.sh
git commit -m "Add the Panama Home Assistant bridge"
```
---
### Task 3: Quickshell service state and Ongoing integration
**Files:**
- Create: `config/dot/quickshell/services/KdeConnect.qml`
- Create: `config/dot/quickshell/services/HomeAssistant.qml`
- Create: `tests/quickshell/control-center-services-contract.sh`
- Modify: `config/dot/quickshell/services/DeviceEvents.qml`
- Modify: `config/dot/quickshell/services/Ongoing.qml`
- Modify: `config/dot/quickshell/shell.qml`
**Interfaces:**
- `KdeConnect`: `available`, `devices`, `preferredPhone`, `phoneReachable`, `transferActive`, `transferFileName`, `recentExchange`, `refresh()`, `sendFile(path)`, `sendClipboard()`, `ring()`, `cancelTransfer()`, `applyFixture(name)`, `clearFixture()`.
- `HomeAssistant`: `phase`, `entities`, `visibleEntities`, `configuredCount`, `stale`, `busyEntityId`, `lastError`, `refresh()`, `toggleEntity(id)`, `open()`, `applyFixture(name)`, `clearFixture()`.
- IPC targets: `kdeconnect` and `home-assistant` with fixture, reset, refresh, action-safe diagnostics, and status functions.
- [ ] **Step 1: Write the failing live fixture contract**
Create a shell test that asserts both IPC targets, then exercises synthetic
fixtures only:
```bash
qs ipc call kdeconnect fixture reachable >/dev/null
jq -e '.fixture and .reachable and .actionCount == 4 and .transferActive == false' \
<<<"$(qs ipc call kdeconnect status)" >/dev/null
qs ipc call kdeconnect fixture transfer >/dev/null
jq -e '.transferActive and .ongoingCount >= 1' \
<<<"$(qs ipc call kdeconnect status)" >/dev/null
qs ipc call home-assistant fixture ready >/dev/null
jq -e '.fixture and .phase == "ready" and .configuredCount == 7 and .visibleCount == 4' \
<<<"$(qs ipc call home-assistant status)" >/dev/null
qs ipc call home-assistant fixture stale >/dev/null
jq -e '.phase == "degraded" and .stale and .configuredCount == 7' \
<<<"$(qs ipc call home-assistant status)" >/dev/null
```
Cleanup resets both fixture modes and closes Quick Settings.
- [ ] **Step 2: Run the contract and verify RED**
Run `tests/quickshell/control-center-services-contract.sh`.
Expected: FAIL because the IPC targets do not exist.
- [ ] **Step 3: Implement `KdeConnect.qml`**
Use one status `Process`, one action `Process`, and one low-frequency `Timer`.
Parse one JSON object per completion. Fixture data contains only synthetic names.
Expose `preferredPhone` as the first reachable `type === "phone"`, then the
first paired phone. Keep action commands as arrays:
```qml
actionProcess.command = [helperPath, "send-file", phone.id, path]
actionProcess.running = true
```
While `send-file` runs, set `transferActive`, basename-only
`transferFileName`, and the device name. On success, set `recentExchange` and
publish one important `StatusEvents` completion with an `open-path` action only
when a local path is safe to reopen. Ring and clipboard completion publish no
event. `cancelTransfer()` terminates only the owned action process.
- [ ] **Step 4: Implement `HomeAssistant.qml`**
Use separate refresh and action `Process` objects. Preserve `entities` when a
refresh fails after a successful snapshot and set `stale=true`. Use one
`busyEntityId` because the UI allows one Home request at a time. After a
successful toggle, immediately run `refresh()`. Expose the first four entities
as `visibleEntities` and retain all configured entities for expansion.
- [ ] **Step 5: Wire shared activity and transitions**
Replace `DeviceEvents.qml`'s private KDE polling process with properties and
change handlers bound to `KdeConnect.phoneReachable` and the preferred phone's
name. Add this activity to `Ongoing.activities`:
```qml
if (KdeConnect.transferActive) {
result.push({
kind: "phone-transfer",
glyph: "\u{F03F2}",
label: "Sending to " + KdeConnect.transferDeviceName,
detail: KdeConnect.transferFileName,
state: "Sending",
tone: "accent",
action: "Cancel"
});
}
```
Route `Ongoing.invoke("phone-transfer")` to `KdeConnect.cancelTransfer()`.
Add typed diagnostic IPC methods without any command that performs a live Home
toggle, Ring, clipboard send, or file send.
- [ ] **Step 6: Restart Quickshell and run the service contract GREEN**
Run the repository's established shell reload command, wait for the current
instance to report IPC, then run:
```bash
tests/quickshell/control-center-services-contract.sh
```
Expected: PASS with fixture state only and no QML warnings in the fresh journal.
- [ ] **Step 7: Commit Task 3**
```bash
git add config/dot/quickshell/services/KdeConnect.qml \
config/dot/quickshell/services/HomeAssistant.qml \
config/dot/quickshell/services/DeviceEvents.qml \
config/dot/quickshell/services/Ongoing.qml \
config/dot/quickshell/shell.qml \
tests/quickshell/control-center-services-contract.sh
git commit -m "Add Control Center integration services"
```
---
### Task 4: Approved Control Center interface
**Files:**
- Create: `config/dot/quickshell/modules/quicksettings/ControlSectionHeader.qml`
- Create: `config/dot/quickshell/modules/quicksettings/HomeControls.qml`
- Create: `config/dot/quickshell/modules/quicksettings/HomeTile.qml`
- Create: `config/dot/quickshell/modules/quicksettings/PhoneControls.qml`
- Create: `config/dot/quickshell/modules/quicksettings/RecentExchange.qml`
- Create: `tests/quickshell/control-center-contract.sh`
- Modify: `config/dot/quickshell/config/Theme.qml`
- Modify: `config/dot/quickshell/modules/quicksettings/QuickSettings.qml`
- Modify: `config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml`
- Modify: `config/dot/quickshell/modules/bar/StatusCluster.qml`
**Interfaces:**
- `HomeControls.expanded: bool` and signal `toggleExpanded()`.
- `PhoneControls.expanded: bool` and signal `toggleExpanded()`.
- Existing `expandedSection` remains the only expansion owner.
- `StatusCluster` remains one click target and reads `KdeConnect.phoneReachable`.
- [ ] **Step 1: Write the failing panel contract**
Assert source and live behavior:
```bash
rg -q 'readonly property int controlCenterWidth: 430' config/dot/quickshell/config/Theme.qml
rg -q 'readonly property int controlCenterTopGap: 2' config/dot/quickshell/config/Theme.qml
rg -q 'margins.top: Theme.barHeight + Theme.controlCenterTopGap' config/dot/quickshell/modules/quicksettings/QuickSettings.qml
rg -q 'HomeControls' config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml
rg -q 'PhoneControls' config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml
rg -q 'visible: KdeConnect.phoneReachable' config/dot/quickshell/modules/bar/StatusCluster.qml
```
Load ready fixtures, open Quick Settings, and assert `hyprctl layers` contains
`qs-popover-quicksettings`. Use IPC status to assert opening Home followed by
Phone leaves only Phone expanded.
- [ ] **Step 2: Run the contract and verify RED**
Run `tests/quickshell/control-center-contract.sh`.
Expected: FAIL on the missing geometry tokens and components.
- [ ] **Step 3: Add geometry and composition**
Add exact tokens:
```qml
readonly property int controlCenterWidth: 430
readonly property int controlCenterTopGap: 2
```
Set both Quick Settings implicit widths to `Theme.controlCenterWidth`, set top
margin to `Theme.barHeight + Theme.controlCenterTopGap`, and retain the current
right margin. On open, call `KdeConnect.refresh()` and
`HomeAssistant.refresh()` before restarting the entrance animation.
Place Home after audio/brightness and Phone after Home, before the footer.
Reuse `expandedSection` values `home` and `phone`; do not introduce nested
popovers.
- [ ] **Step 4: Implement the Home components**
`HomeTile.qml` is a button-like rectangle with `entity`, `busy`, and
`onActivated`. It maps `light` to a lamp glyph, uses `Theme.warn` at low alpha
only when `entity.active`, and shows `Unavailable` for `!entity.available`.
`HomeControls.qml` uses a four-column resting grid from
`HomeAssistant.visibleEntities`. Expanded mode uses a single-column list of all
`HomeAssistant.entities`. Include deterministic loading, setup,
authentication-required, unreachable/stale, and empty states. Retry calls
`HomeAssistant.refresh()`; Open calls `HomeAssistant.open()`.
- [ ] **Step 5: Implement the Phone components and chooser**
`PhoneControls.qml` renders the approved identity-first card. Show only actions
included in `preferredPhone.actions`. Use `QtQuick.Dialogs.FileDialog` with a
single existing-file selection. `onAccepted` passes the selected local file path
as a distinct argument to `KdeConnect.sendFile()`; `onRejected` does nothing.
Disabled/offline copy is `Not nearby`. Missing KDE Connect copy is
`KDE Connect unavailable`. `RecentExchange.qml` is visible only when the
service has a real current-session exchange.
- [ ] **Step 6: Add reachable-phone status and visual states**
Append one phone glyph to the existing `StatusCluster` only when
`KdeConnect.phoneReachable`; it has no MouseArea and inherits the parent Pill's
activation. Use `Theme.cyan` for reachable, no disconnected glyph, and no
animation.
- [ ] **Step 7: Restart Quickshell and run the panel contract GREEN**
Run `tests/quickshell/control-center-contract.sh` and inspect the fresh shell
journal for `error`, `warning`, `ReferenceError`, and `TypeError` entries from
the current Quickshell start.
Capture resting, Home-expanded, Phone-offline, transfer-active, and Home-stale
fixture screenshots to `/tmp`; do not commit screenshots.
- [ ] **Step 8: Commit Task 4**
```bash
git add config/dot/quickshell/config/Theme.qml \
config/dot/quickshell/modules/quicksettings \
config/dot/quickshell/modules/bar/StatusCluster.qml \
tests/quickshell/control-center-contract.sh
git commit -m "Build the Panama Control Center"
```
---
### Task 5: Documentation and full verification
**Files:**
- Modify: `config/dot/hypr/DESKTOP-PARITY.md`
- Modify: `config/dot/hypr/README.md`
**Interfaces:**
- Consumes the complete helper, service, UI, and IPC contracts from Tasks 14.
- Produces an auditable verification result and an up-to-date remaining roadmap.
- [ ] **Step 1: Update desktop documentation**
Mark Home Assistant favourites and KDE Connect phone integration Live. Document
the 430-pixel Control Center, two-pixel bar attachment, environment/keyring
configuration precedence, BlueBubbles messaging boundary, and current iOS
capabilities. Remove those two items from `System UI still worth adding` while
retaining printers and the external RustDesk lock-screen test.
- [ ] **Step 2: Run every focused test**
```bash
python3 tests/quickshell/kdeconnect_bridge_test.py
python3 tests/quickshell/home_assistant_bridge_test.py
tests/quickshell/kdeconnect-helper-contract.sh
tests/quickshell/home-assistant-helper-contract.sh
tests/quickshell/control-center-services-contract.sh
tests/quickshell/control-center-contract.sh
```
Expected: every command exits 0; live contracts print counts only.
- [ ] **Step 3: Run full Panama verification**
Run `hyprctl configerrors` and require empty output. Run every executable
`tests/quickshell/*contract.sh` in sorted order and require zero failures.
Restart Quickshell from a clean process, verify all IPC targets return, and
inspect only the new process's journal for QML errors or warnings.
- [ ] **Step 4: Perform read-only live checks**
Verify the Control Center reports one reachable phone with the confirmed action
names and seven configured Home favourites without printing identities. Open
and close the panel, expand Home and Phone, and confirm Wi-Fi, Bluetooth, audio,
Focus, Caffeine, Night Light, settings, power, Escape, and outside-click remain
usable. Do not trigger an external-device action.
- [ ] **Step 5: Review the final diff against the spec**
Check every Goal, Non-goal, Interaction, Security, Failure, Testing, and
Documentation requirement in
`docs/superpowers/specs/2026-08-17-panama-control-center-design.md`. Run
`git diff --check` and confirm no secret-shaped value, live entity response,
device ID, IP address, or absolute personal file path entered tracked content.
- [ ] **Step 6: Commit documentation and push the feature**
```bash
git add config/dot/hypr/DESKTOP-PARITY.md config/dot/hypr/README.md
git commit -m "Document the Panama Control Center"
git push origin main
```
- [ ] **Step 7: Offer the three explicit live action checks**
Ask the user to choose actions from the finished Control Center: toggle one Home
light, Ring the iPhone, send clipboard text, or send a harmless chosen file.
Treat the visual state and resulting external device behavior as the acceptance
signal; do not execute an external action from diagnostics or automation.