Skip to content

Commit a914319

Browse files
committed
Build multi-device GUI dashboard foundation
Add saved Time Capsule profiles, Keychain-backed passwords, profile-scoped backend execution, and the dashboard/add-device app shell. Unify the GUI and CLI discovery contract around deduped device candidates, preserve root SSH targeting, and document the target GUI architecture. Fix operation ownership so rejected starts do not enter running states and dashboard snapshots are attributed to the profile that started the operation.
1 parent 30d69e2 commit a914319

43 files changed

Lines changed: 4962 additions & 245 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

GUI_ARCH.md

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
# TimeCapsuleSMB GUI Architecture
2+
3+
This is the living architecture target for the macOS GUI. Future GUI changes
4+
should reference this file and keep the implementation moving toward these
5+
boundaries.
6+
7+
## Product Shape
8+
9+
The GUI is a native multi-device manager for Apple Time Capsules. It should not
10+
feel like a wrapper around CLI commands.
11+
12+
The main user flows are:
13+
14+
1. Add one or more Time Capsules.
15+
2. Save device profiles with per-device config files.
16+
3. Store passwords in Keychain only.
17+
4. Install or update SMB support.
18+
5. Run checkups and show structured health.
19+
6. Run maintenance tasks with explicit plans and confirmations.
20+
7. Surface advanced logs and helper details only when needed.
21+
22+
`bootstrap`, `paths`, and `validate-install` are app readiness concerns. They
23+
run in the background or diagnostics surfaces, not as first-class user actions.
24+
The bundled app should already contain the helper, runtime, tools, artifacts,
25+
and manifests needed by those checks.
26+
27+
## Architectural Principles
28+
29+
- The app is profile-first. Screens operate on `DeviceProfile`, not loose host
30+
fields or a shared `.env`.
31+
- Views are thin. They render state and send user intents to stores.
32+
- Stores own state machines. Each workflow has explicit states, terminal states,
33+
validation, and event-to-model parsing.
34+
- Backend execution is centralized. There is one global `OperationCoordinator`
35+
and one active helper operation at a time.
36+
- Backend contracts are typed at the GUI boundary. Swift decodes payloads into
37+
models and does not parse human log text for app behavior.
38+
- Credentials never persist to `.env`. GUI passwords live in Keychain and are
39+
passed per operation as credentials.
40+
- Runtime context is explicit. Profile-scoped operations always carry
41+
`DeviceRuntimeContext`.
42+
- Device snapshots are attributed to the operation profile ID, not the currently
43+
selected sidebar item.
44+
- Advanced diagnostics exist, but normal workflows use user-facing language:
45+
Install / Update, Checkup, Maintenance, Add Time Capsule.
46+
47+
## Layer Map
48+
49+
Target source organization:
50+
51+
```text
52+
TimeCapsuleSMBApp/
53+
App/
54+
AppStore.swift
55+
AppReadinessStore.swift
56+
Backend/
57+
BackendClient.swift
58+
BackendPayloads.swift
59+
HelperLocator.swift
60+
HelperRunner.swift
61+
OperationCoordinator.swift
62+
OperationParams.swift
63+
PendingConfirmation.swift
64+
Profiles/
65+
DeviceProfile.swift
66+
DeviceRegistryStore.swift
67+
PasswordStore.swift
68+
Policies/
69+
HostCompatibilityPolicy.swift
70+
Workflows/
71+
AddDeviceFlowStore.swift
72+
DashboardStore.swift
73+
DeployWorkflowStore.swift
74+
DoctorStore.swift
75+
MaintenanceStore.swift
76+
Views/
77+
Shell/
78+
AddDevice/
79+
Dashboard/
80+
Diagnostics/
81+
Components/
82+
```
83+
84+
The current code can keep file names during transition, but new substantial
85+
screen code should move toward this split instead of growing `ContentView.swift`.
86+
87+
## Ownership
88+
89+
### AppStore
90+
91+
`AppStore` is the app composition root. It owns:
92+
93+
- `AppReadinessStore`
94+
- `DeviceRegistryStore`
95+
- `OperationCoordinator`
96+
- `PasswordStore`
97+
- selected profile ID
98+
- high-level navigation state
99+
100+
`AppStore` should not parse backend events. It may derive cross-cutting summary
101+
state such as the dashboard primary action, host compatibility warnings, and
102+
password availability.
103+
104+
### DeviceRegistryStore
105+
106+
`DeviceRegistryStore` owns persistent device profiles:
107+
108+
```text
109+
~/Library/Application Support/TimeCapsuleSMB/devices.json
110+
~/Library/Application Support/TimeCapsuleSMB/Devices/<device-id>/.env
111+
```
112+
113+
The registry is responsible for:
114+
115+
- loading and saving `devices.json`
116+
- creating per-device config directories
117+
- duplicate matching by Bonjour fullname and normalized host
118+
- deleting profile config directories
119+
- persisting checkup and deploy snapshots
120+
121+
It must not delete corrupt registries automatically. Corrupt registry state
122+
goes to diagnostics and waits for explicit user recovery.
123+
124+
### PasswordStore
125+
126+
`PasswordStore` abstracts Keychain access.
127+
128+
Production storage:
129+
130+
```text
131+
service = TimeCapsuleSMB.DevicePassword
132+
account = <DeviceProfile.id>
133+
```
134+
135+
Rules:
136+
137+
- Add Device saves a password only after `configure` succeeds.
138+
- `.env` files never contain `TC_PASSWORD`.
139+
- Missing Keychain item maps to `passwordNeeded` or `.missing`.
140+
- Keychain access errors map to `.keychainUnavailable`.
141+
- Auth failures mark the password invalid, but do not delete it automatically.
142+
- Forget Device deletes the profile, per-device config directory, and Keychain
143+
item as one user-visible action.
144+
145+
## Backend Execution
146+
147+
`BackendClient` owns process execution state and raw events. It should not know
148+
about UI screens.
149+
150+
`OperationCoordinator` is the only workflow-facing entry point for helper runs:
151+
152+
```swift
153+
run(operation:params:profile:password:)
154+
run(operation:params:context:activeDeviceID:password:)
155+
```
156+
157+
Responsibilities:
158+
159+
- reject a second operation while one is running
160+
- expose active operation and active profile ID
161+
- inject password credentials when provided
162+
- delegate profile context to `BackendClient`
163+
- preserve context through confirmation replay
164+
- support cancel and clear semantics
165+
166+
Profile-scoped operations must pass `DeviceRuntimeContext`. The backend layer
167+
injects:
168+
169+
- `params["config"] = context.configURL.path`
170+
- `TCAPSULE_CONFIG = context.configURL.path`
171+
172+
`TCAPSULE_STATE_DIR` remains app-level so bootstrap/version/cache state is not
173+
multiplied per profile.
174+
175+
## Operation Attribution
176+
177+
Workflow stores must attribute terminal results to the profile that started the
178+
operation.
179+
180+
Do not write snapshots using `selectedProfile` at result time. The user can
181+
change sidebar selection while an operation runs. A workflow should capture
182+
`activeProfileID` when it starts, then use that ID when persisting:
183+
184+
- `DeviceCheckupSnapshot`
185+
- `DeviceDeploySnapshot`
186+
- future maintenance snapshots
187+
188+
If `OperationCoordinator` rejects a run, the caller must leave or restore its
189+
state to a non-running failure state. No workflow should enter `running`,
190+
`planning`, `configuring`, or `saving` unless the operation actually started.
191+
192+
## Backend Contract
193+
194+
The Python app API is the source of truth for structured payloads. GUI-facing
195+
payloads should remain stable and versioned.
196+
197+
Important contracts:
198+
199+
- `discover` returns `devices`, a deduped list of selectable Time Capsules.
200+
- Each discovered device includes `selected_record`, which the GUI passes back
201+
to `configure`.
202+
- `configure` accepts either `selected_record` or `host`.
203+
- Manual `host` values are treated as root SSH targets by the backend.
204+
- GUI `configure` sends `persist_password: false`.
205+
- Deploy, doctor, activate, uninstall, and fsck receive credentials from
206+
Keychain-backed GUI state.
207+
208+
Swift should prefer decoding structured fields over reading `summary` strings.
209+
Raw summaries are for display only.
210+
211+
## Add Device Flow
212+
213+
Add Device is a state machine with mutually exclusive entry modes:
214+
215+
- Discover
216+
- Manual Address
217+
218+
States:
219+
220+
```text
221+
idle
222+
discovering
223+
discoveryEmpty
224+
discoveryReady
225+
manualEntry
226+
passwordEntry
227+
configuring
228+
savingProfile
229+
saved
230+
authFailed
231+
unsupported
232+
failed
233+
```
234+
235+
Discover mode:
236+
237+
- runs backend `discover`
238+
- shows only `payload.devices`
239+
- auto-selects if there is exactly one device
240+
- fills and disables Host/IP from the selected device
241+
- routes already saved devices to their existing profile
242+
243+
Manual mode:
244+
245+
- clears discovered candidates from the active flow
246+
- enables Host/IP entry
247+
- assumes root SSH unless the user explicitly enters a user
248+
249+
Save rules:
250+
251+
- no profile is saved until `configure` succeeds
252+
- wrong password saves nothing
253+
- unsupported device saves nothing
254+
- duplicate host or Bonjour fullname updates the existing profile
255+
- Keychain save failure may keep the profile, but marks password state missing
256+
257+
## Dashboard
258+
259+
The dashboard has these user-facing tabs:
260+
261+
- Overview
262+
- Install / Update
263+
- Checkup
264+
- Maintenance
265+
- Advanced
266+
267+
Overview is decision-oriented. It shows device identity, password state, host
268+
macOS warnings, last checkup, last install/update, and one primary action.
269+
270+
Install / Update wraps deploy planning and deploy execution. Dry-run planning
271+
should remain first-class.
272+
273+
Checkup wraps doctor and shows grouped checks by domain and status.
274+
275+
Maintenance wraps:
276+
277+
- NetBSD4 activation
278+
- uninstall
279+
- fsck
280+
- repair xattrs
281+
- future flash workflow
282+
283+
Advanced contains raw events, helper path, profile ID, config path, and other
284+
technical diagnostics.
285+
286+
## App Readiness And Bundling
287+
288+
Readiness runs at app launch and validates the bundled runtime. It is not a
289+
device workflow.
290+
291+
Production bundle target:
292+
293+
```text
294+
Contents/MacOS/TimeCapsuleSMB
295+
Contents/Helpers/tcapsule
296+
Contents/Resources/Distribution/...
297+
Contents/Resources/Tools/...
298+
```
299+
300+
The app sets:
301+
302+
- `TCAPSULE_CONFIG` per profile operation
303+
- `TCAPSULE_STATE_DIR` to app support
304+
- `TCAPSULE_DISTRIBUTION_ROOT` to bundled distribution resources
305+
- `PATH` to bundled tools where required
306+
307+
If bundled resources are missing or invalid, normal workflows are blocked and
308+
diagnostics explain that the app install is incomplete.
309+
310+
## Host Compatibility
311+
312+
`HostCompatibilityPolicy` is pure Swift and side-effect free. It warns
313+
non-blockingly for host macOS versions with known Time Machine network backup
314+
issues:
315+
316+
- macOS 15.7.5
317+
- macOS 15.7.6
318+
- macOS 15.7.7
319+
- macOS 26.4.x
320+
321+
Warnings appear globally or on dashboards, but they do not prevent SMB install
322+
or maintenance.
323+
324+
## Error Handling
325+
326+
Errors should preserve machine-readable codes and user-facing recovery.
327+
328+
Workflow stores should map backend errors into:
329+
330+
- state transition
331+
- concise visible message
332+
- recovery action, when available
333+
- raw details in Advanced or Diagnostics
334+
335+
Authentication failures must prompt for password replacement without deleting
336+
the existing Keychain item automatically.
337+
338+
Unsupported devices must show the compatibility explanation and avoid creating
339+
profiles.
340+
341+
## Testing Standards
342+
343+
Every workflow state enum should have an inventory test. Tests should verify
344+
state transitions and side effects through mocks, not string grep checks.
345+
346+
Required coverage areas:
347+
348+
- missing, corrupt, save, update, duplicate, and delete registry behavior
349+
- Keychain save/read/update/delete, missing item, and unavailable item
350+
- backend context injection and confirmation replay context preservation
351+
- operation rejection while another operation is active
352+
- add-device discover/manual/auth/unsupported/duplicate/password-save failure
353+
- dashboard primary action derivation
354+
- operation snapshots attributed to active operation profile ID
355+
- host compatibility warning matrix
356+
- helper locator production and development environment behavior
357+
358+
Regression runs:
359+
360+
```bash
361+
cd macos/TimeCapsuleSMB && swift test
362+
.venv/bin/pytest
363+
```
364+
365+
Run Python tests from the repo root. Run Swift tests from
366+
`macos/TimeCapsuleSMB`.

0 commit comments

Comments
 (0)