@mhome/app-facade-protocol 1.118.4 → 1.125.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +21 -0
  2. package/contract/client-space-focus-v1.md +1 -1
  3. package/contract/device-presentation-v1.md +35 -0
  4. package/contract/entity-v1.md +53 -0
  5. package/contract/pod-commissioning-v1.md +12 -12
  6. package/contract/reference-presentation-v1.md +1 -1
  7. package/contract/resource-read-v1.md +11 -11
  8. package/contract/trace-v1.md +17 -0
  9. package/contract/user-navigation-v1.md +47 -0
  10. package/fixtures/authorization/decisions.json +110 -110
  11. package/fixtures/billing.conformance.json +49 -2
  12. package/fixtures/card-skins/space.json +1 -1
  13. package/fixtures/entity-presentation.conformance.json +2 -2
  14. package/fixtures/integration-presentation.conformance.json +1 -1
  15. package/fixtures/local-hub-state.configured.json +1 -1
  16. package/fixtures/pod-commission.awaiting-authorization.json +3 -3
  17. package/fixtures/presentation/cases.json +666 -35
  18. package/fixtures/recording.conformance.json +15 -8
  19. package/fixtures/routing/cases.json +44 -44
  20. package/fixtures/trace/linear.json +6 -0
  21. package/fixtures/widget-presentation.v1.json +3 -3
  22. package/manifest/app-facade.v1.json +49 -6
  23. package/manifest/authorization.v1.json +17 -8
  24. package/manifest/host-provision-targets.v1.json +1 -1
  25. package/manifest/hub-targets.v1.json +1 -1
  26. package/manifest/navigation-cards.v1.json +29 -0
  27. package/manifest/pod-targets.v1.json +2 -2
  28. package/manifest/presentation-times.v1.json +377 -29
  29. package/manifest/recording.v1.json +18 -15
  30. package/manifest/routing.v1.json +5 -4
  31. package/manifest/targets.v1.json +1 -1
  32. package/package.json +1 -1
  33. package/protocol.json +2 -1
  34. package/schema/delegated-call.v1.schema.json +2 -2
  35. package/schema/local-hub-state.v1.schema.json +2 -2
  36. package/schema/pod-commission.v1.schema.json +6 -6
  37. package/schema/routing.v1.schema.json +3 -3
  38. package/schema/system-hosts.v1.schema.json +2 -2
  39. package/schema/trace.v1.schema.json +278 -0
  40. package/schema/user-navigation.v1.schema.json +133 -0
package/README.md CHANGED
@@ -22,6 +22,8 @@ identity. Core-to-Node routes and provider runtime models belong to
22
22
 
23
23
  See [resource definition, observation, history and relationship reads](contract/resource-read-v1.md)
24
24
  for shared business reads and the separate UI rendering boundary.
25
+ Device-only rendering, online rows and chart series use the
26
+ [Device UI presentation contract](contract/device-presentation-v1.md).
25
27
 
26
28
  See [Agent LLM settings](contract/agent-llm-v1.md) for active-mode reads,
27
29
  independent saved configuration and Provider Operation boundaries.
@@ -47,6 +49,25 @@ results and compose real Facade calls; they are not alternative DTOs for a singl
47
49
  Facade operation. A UI view model may derive display state after the SDK boundary.
48
50
  Composition does not imply a distributed transaction or automatic rollback.
49
51
 
52
+ ## Full values and Essential presentation
53
+
54
+ Full preserves canonical typed values, numeric instants and source units. Business execution,
55
+ caching, aggregation, media validation and UI consumers use Full. Only Essential replaces
56
+ explicitly tagged runtime primitives with display text. Numeric timestamp fields use the
57
+ shared `manifest/presentation-times.v1.json` paths and declared units; arbitrary numbers,
58
+ field names and provider payloads are never guessed. Declared decimal-string Storage times
59
+ are rendered at their explicit paths; Full keeps the Storage wire types.
60
+
61
+ Timestamp text includes Home's IANA timezone, offset and millisecond precision when present.
62
+ Missing or invalid Home timezone uses UTC. User/connection operations have no Space presentation context and use UTC/source units; an incidental Space ID never grants Home/preference reads. Scope temperature/length/weight preferences apply
63
+ only to Essential unit display. Native Agent workflows extract typed facts and media from Full
64
+ before rendering final model history with the same rules, using structured context facts tied
65
+ to the user-scope generation. Prepared control objects are not business display data.
66
+
67
+ Core and cloud direct calls resolve presentation context before executing an Essential write.
68
+ MCP workflow projection preserves already settled outcomes if its optional context lookup fails:
69
+ it adds a presentation warning, uses UTC and retains source units, without replaying the operation.
70
+
50
71
  ## App result contract
51
72
 
52
73
  `control.responseDetail` selects information depth: `full` (default) returns
@@ -15,7 +15,7 @@ Both request-response (`arq`) and one-way state synchronization (`ad`) use this
15
15
  same envelope. Native switch orchestration retains its existing ACK and session
16
16
  fences. Background synchronization remains per physical connection and is not
17
17
  an additional user switch. It never replays an obsolete clear after reconnect.
18
- For Cloud clear, the existing envelope `scopeId`, when non-empty, is a condition:
18
+ For Cloud clear, the existing envelope `spaceId`, when non-empty, is a condition:
19
19
  a delayed clear for A cannot erase a newer focus on B. Omitted/blank scope clears
20
20
  unconditionally. Local connections can only focus/clear their bound Space.
21
21
 
@@ -0,0 +1,35 @@
1
+ # Device UI presentation
2
+
3
+ Device-specific UI adapters use `/app/presentation/device/*`. All three are
4
+ read-only, use the standard App Facade envelope and follow Space-mode execution:
5
+ cloud Spaces execute in the cloud; local Spaces execute on their selected Hub.
6
+ They have no native Agent, external CLI Skill, MCP or CLI entrypoint. Shared
7
+ `/app/device/state/get`, list, detail and bounded history remain domain APIs.
8
+
9
+ - `/app/presentation/device/view/get`, `{deviceId}`: `{deviceId, primaryViewId, views, widgets, twin}`.
10
+ Reads the unique hidden Device render definition, preserving exact Widget/Twin
11
+ bindings. Disabled devices can have empty definitions. Multiple anchors fail rather
12
+ than choosing an arbitrary visible Card. Widgets contain configuration, never values. The optional `twin` metadata is
13
+ returned even when a known device has no render anchor.
14
+ - `/app/presentation/device/history/series/get`, `{deviceId, capabilityIds?: []}`:
15
+ `{deviceId, series}` with per-capability decoded `states`, `events`, `logs`. Empty
16
+ capability selection reads all series. This preserves chart structure; the shared
17
+ Device history Facade remains a bounded, merged business timeline.
18
+ - `/app/presentation/device/online/list`, `{entityIds?: []}`:
19
+ `{devices:[{entityId,online,updatedAtSec}]}`. Normalizes and deduplicates selected
20
+ IDs, preserves request order and returns only IDs with stored online records.
21
+ An empty selection returns no rows. This preserves the existing UI projection:
22
+ `online` is true for the stored `online` observation and false for other stored
23
+ states; an absent record is omitted. It never probes a device or reads capability
24
+ observations. The shared state API instead omits unknown `online` fields.
25
+
26
+ Device details and Device Group State tabs read static bindings first, then use
27
+ shared selected `/app/device/state/get` and `/app/widget/value/get` observations. Canvas
28
+ content reuses static Card assembly; it does not call the single-device view API.
29
+ History renderers request chart series only when needed. The Devices directory
30
+ reads selected online rows after loading its catalog. These reads never refresh
31
+ providers, change Twin lifecycle or initialize new business resources.
32
+
33
+ The backend adapters own visibility, unique render-anchor validation and existing
34
+ series filtering. Managers, persistence tables, group aggregation, generated
35
+ Automations and push protocols are unchanged. Only the namespaced UI routes are registered.
@@ -0,0 +1,53 @@
1
+ # General Entity reads
2
+
3
+ All reads use authenticated tenant and Space. Device, Automation and Widget types
4
+ belong to their dedicated domains. Location and concrete AppClient endpoints are
5
+ built-in Entity types; other types are Integration-defined.
6
+
7
+ - `/app/entity/list`: `{entityType, includeGroups?, integrationId?, connectId?}` returns `{entities: [EntityMetadata]}` for the
8
+ exact type, including same-type Groups by default. `includeGroups:false` reads only
9
+ ordinary entities. A Provider filter selects ordinary source resources only;
10
+ `connectId` requires `integrationId`. There is one list, with no mode or refresh.
11
+ - `/app/entity/detail/get`: `{entityType, ids}` returns ordered deduplicated
12
+ `{items: [{requestedId, found, reason?, entity?}]}`. `entity` is exactly the same
13
+ metadata model as a list row; callers with known IDs need not list first.
14
+ - `/app/entity/state/get`: `{entityType, ids}` (1–100 nonblank IDs before
15
+ deduplication) returns ordered `{items: [{requestedId, found, reason?, resource?}]}`.
16
+ `resource` is `{entityId, latest: [{capabilityId, state?, event?, log?}]}`.
17
+ These are latest persisted observations, without refreshing a Provider or deriving
18
+ online status. Groups read their own observations without expanding members.
19
+ Existing Location/AppClient endpoints return `latest: []`; missing resources use
20
+ `found: false, reason: "not_found"` and omit resource. No observations is not missing.
21
+ - `/app/entity/history/list` remains independent and returns at most 100 records.
22
+
23
+ Metadata contains id, name, entityType, integration?, connectId?, active, capabilities, data,
24
+ tags (Blueprint IDs), availabilityStatus?, issue, group and entityIds (Group members).
25
+ It contains no current capability values. Full retains the complete metadata.
26
+ Essential omits Blueprint tags, ordinary entityIds, absent optional values and
27
+ redundant built-in data identity fields. It preserves arbitrary Integration data,
28
+ including Provider-local data.id, falsy values and Group entityIds. State payloads
29
+ are not trimmed; only declared observation timestamps are localized by the shared
30
+ presentation-time rules. No arbitrary business-data timestamp inference is used.
31
+
32
+ List, detail and state respect installed Provider and persisted source visibility.
33
+ Ordinary reads accept Provider-owned Twins only. Groups are read from independent
34
+ definitions; their derived Twins cannot masquerade as ordinary entities.
35
+ Provider-defined types resolve installation through their parent Integration.
36
+ Built-ins are scoped to the current Space and reject Provider filters.
37
+
38
+ ## Device sources
39
+
40
+ `/app/device/list` accepts `{includeGroups?, integrationId?, connectId?}` with the
41
+ same source filter semantics. Its rows contain standard Device metadata, never
42
+ card relationships or current observations. UI Resources and House compose domain
43
+ reads; they do not require a multi-type backend catalog.
44
+
45
+ `/app/device/source/resolve` is a read-only onboarding helper. Input is
46
+ `{integrationId, connectId?, sourceIds}` (1–100 nonblank opaque IDs before
47
+ ordered deduplication). A connected Provider requires one exact Connection; a
48
+ direct Provider omits it. Each result is `{requestedSourceId, found, entityId?,
49
+ reason?}`. Missing IDs return `not_found`; multiple canonical IDs for one source
50
+ identity fail the request. Only installed, visible Provider-owned Devices match.
51
+ The operation never synchronizes sources or returns metadata, state or cards.
52
+ Source synchronization remains `/app/integration/entity/refresh`; onboarding reads
53
+ standard Device metadata only after an explicitly successful synchronization.
@@ -135,7 +135,7 @@ the response also carries `"mode":"reprovision"` and `wifiConfigured` is false
135
135
 
136
136
  `pod-credential` requests:
137
137
 
138
- - `{"op":"deliver","clientMode":"user","podId","loginGrant","scopeId","cloudApi","cloudWs"}` →
138
+ - `{"op":"deliver","clientMode":"user","podId","loginGrant","spaceId","cloudApi","cloudWs"}` →
139
139
  `{"ok":true,"state":"activating"}`. Accepted only in `wifi_connected` outside a
140
140
  Wi-Fi change window; otherwise `{"ok":false,"state":<current pod state>,"reason"}`
141
141
  with `reason` `invalid_payload` (a missing or malformed field, including a
@@ -241,7 +241,7 @@ Lion returns `{podId, loginGrant, activateBefore}`. The random approval expires
241
241
  in ten minutes and is bound to this User and the Pod identity key. It is not a
242
242
  login refresh token. The helper must never deliver its own login credentials.
243
243
 
244
- The App delivers the approval through the paired LAN channel. `scopeId` in the
244
+ The App delivers the approval through the paired LAN channel. `spaceId` in the
245
245
  device delivery selects the initial UI Space; it does not constrain the User
246
246
  identity or grant Space authority. The device independently redeems the approval
247
247
  at `POST /api/v1/pod/login/redeem` with `{podId, loginGrant, proof}`. The proof is
@@ -304,7 +304,7 @@ never show these values or `message` verbatim.
304
304
  | `invalid_wifi` | SSID not 1–32 bytes, or password not empty or 8–63 characters (64 hex digits) |
305
305
  | `bluetooth_unavailable` | no usable Bluetooth adapter |
306
306
  | `not_signed_in` | no signed-in MeowLink account |
307
- | `scope_not_offered` | the Space is not among those offered, or the account has no Space |
307
+ | `space_not_offered` | the Space is not among those offered, or the account has no Space |
308
308
  | `cloud_unreachable_for_device` | the cloud address is loopback |
309
309
  | `busy` | the native Client cannot take the request now; retry |
310
310
 
@@ -318,7 +318,7 @@ never show these values or `message` verbatim.
318
318
  | `/local/pod/commission/code` | `{"sessionId","code"}` | session snapshot |
319
319
  | `/local/pod/commission/wifi` | `{"sessionId","ssid","password"?}` | session snapshot |
320
320
  | `/local/pod/commission/wifi/scan` | `{"sessionId"}` | session snapshot |
321
- | `/local/pod/commission/authorize` | `{"sessionId","scopeId"}` | session snapshot |
321
+ | `/local/pod/commission/authorize` | `{"sessionId","spaceId"}` | session snapshot |
322
322
  | `/local/pod/commission/status` | `{}` | `{"session": snapshot or null}` |
323
323
  | `/local/pod/bluetooth/settings` | `{}` | `{"opened"}` |
324
324
 
@@ -346,7 +346,7 @@ page does not end setup; returning to it reads `status`. Waiting for user input
346
346
  also counts toward the deadline. Discovery leases still control scanning.
347
347
 
348
348
  Sign-in and Spaces are checked at `start` (`not_signed_in`,
349
- `scope_not_offered`) and again when the authorization prompt is built.
349
+ `space_not_offered`) and again when the authorization prompt is built.
350
350
 
351
351
  A pod inside a Wi-Fi change window (`pod-info` `"mode":"reprovision"`) gets a
352
352
  Wi-Fi-only session. From `reading_info` on, its snapshots carry
@@ -409,7 +409,7 @@ The schemas are `schema/pod-discovery.v1.schema.json` and
409
409
  ## 8. Space Client local credential
410
410
 
411
411
  `POST /api/v1/pod/space/credential/issue` uses the assisting App's user
412
- `Authorization` and `ScopeId`, with the same device identity request as personal
412
+ `Authorization` and `SpaceId`, with the same device identity request as personal
413
413
  issuance. Only the current Space owner may assist. The Space must be local and have one
414
414
  published Hub with business and Host TLS identities. Cloud Spaces must complete
415
415
  Hub setup first. The issuer derives `appClientId` from tenant, Space and the validated
@@ -417,12 +417,12 @@ persistent device-key fingerprint. The same device in the same Space retains thi
417
417
  ID across retries; a factory reset rotates its key. Candidate local JWTs use the
418
418
  existing Space signing key and local-client audience.
419
419
 
420
- Claims are `token_type: local_space_client_access`, `sub: <scopeId>`,
421
- `tenant_id`, `scope_id`, `hub_id`, `app_client_id`, `jti`, `iat`, `exp`, `is_test`.
420
+ Claims are `token_type: local_space_client_access`, `sub: <spaceId>`,
421
+ `tenant_id`, `space_id`, `hub_id`, `app_client_id`, `jti`, `iat`, `exp`, `is_test`.
422
422
  The expiry follows existing long-lived User local credentials. No `userId`,
423
- initializer identity, `podId` or `issuedScopeId` grants authority. User local
423
+ initializer identity, `podId` or `issuedSpaceId` grants authority. User local
424
424
  credentials retain `local_client_access` and must not contain `app_client_id`.
425
- Personal Pod issuance no longer stores `issuedScopeId`.
425
+ Personal Pod issuance no longer stores `issuedSpaceId`.
426
426
 
427
427
  Lion sends `{jwtToken, clientSource:"pod"}` to the authenticated Hub Bridge
428
428
  `/hub/client/space/register`. Core verifies signature, audience, issuer, current
@@ -437,12 +437,12 @@ The Bridge returns `{credentialId, appClientId, jwtToken, status, activateBefore
437
437
  Lion returns `{credentialId, appClientId, jwtToken, credentialState, activateBefore,
438
438
  hubIdentity}`. State is `pending` with its epoch-millisecond first-use deadline, or
439
439
  `active` with `activateBefore:null`. Issuance alone does not publish a device-list entry. The Hub identity includes its business P-256 key and
440
- Host TLS public key, bound to `tenantId`, `scopeId`, `hubId` and `hostId`.
440
+ Host TLS public key, bound to `tenantId`, `spaceId`, `hubId` and `hostId`.
441
441
 
442
442
  The secured LAN `pod-credential` delivery is:
443
443
 
444
444
  ```json
445
- {"op":"deliver","clientMode":"space","credentialId":"…","appClientId":"a:…","jwtToken":"…","tenantId":"…","scopeId":"…","hubIdentity":{}}
445
+ {"op":"deliver","clientMode":"space","credentialId":"…","appClientId":"a:…","jwtToken":"…","tenantId":"…","spaceId":"…","hubIdentity":{}}
446
446
  ```
447
447
 
448
448
  The actual `hubIdentity` is the complete issuer response. User credential fields
@@ -1,7 +1,7 @@
1
1
  # Reference picker presentation
2
2
 
3
3
  UI-only Space reads use the standard FacadeCall and success/data/error envelope.
4
- Both targets require spaceRead and follow scopeMode routing. They are not native
4
+ Both targets require spaceRead and follow spaceMode routing. They are not native
5
5
  Agent, CLI or MCP tools. Full and Essential preserve the same business fields;
6
6
  Essential represents them as YAML.
7
7
 
@@ -11,6 +11,9 @@ follow the Space execution authority on both local Core and cloud.
11
11
  | --- | --- | --- |
12
12
  | `/app/device/detail/get` | `{ids}` | Existing ordered `items` with `device` definitions, capabilities, group membership, physical Twin enablement or Group application state, source metadata; no online or state fields |
13
13
  | `/app/device/state/get` | `{ids}` | Ordered `items` with `resource: {entityId, online?, onlineUpdatedAtSec?, latest}` |
14
+ | `/app/entity/list` | `{entityType}` | Complete static directory metadata of the exact general Entity type; same-type Groups included; no list modes |
15
+ | `/app/entity/detail/get` | `{entityType, ids}` | Selected metadata identical to list rows, under ordered `items[].entity` |
16
+ | `/app/entity/state/get` | `{entityType, ids}` | Ordered `items` with `resource: {entityId, latest}`; no source refresh or derived online; see [Entity contract](entity-v1.md) |
14
17
  | `/app/widget/list` | `{}` | Summaries: id, displayName, description, type, tag; Data adds dataType/repeated. No list mode or full-definition catalog |
15
18
  | `/app/widget/detail/get` | `{ids}` | Existing ordered `items` with `widget` configuration; no value, next or nextTimestamp |
16
19
  | `/app/widget/value/get` | `{ids}` | Ordered `items` with `resource: {widgetId, revision, value, next?, nextTimestamp?}` |
@@ -30,7 +33,9 @@ Observation batches accept 1–100 IDs, reject blanks, trim and deduplicate pres
30
33
  request order. Every normalized ID gets `{requestedId, found, resource?}`; missing
31
34
  resources have `found: false, reason: "not_found"`. A known Device with no observations
32
35
  has `found: true` and `latest: []`. Unknown reachability omits `online`; it is not false.
33
- Device state supports visible physical devices and Device groups. Automation status
36
+ Device state supports visible physical devices and Device groups. General Entity state
37
+ also requires entityType, supports same-type Groups and respects source visibility;
38
+ known Location/AppClient endpoints return empty latest without querying Twin. Automation status
34
39
  accepts user program IDs, including retained one-time programs, not groups or internal
35
40
  Twin programs. Widget value supports Data, Button and Circadian resources. JSON null
36
41
  is a value, not a missing resource. Button values may be null.
@@ -56,15 +61,10 @@ observation and graph reads preserve their bounded business fields in Essential.
56
61
 
57
62
  ## UI-only reads
58
63
 
59
- - `/app/device/view/get`, `{deviceId}`: `{deviceId, primaryViewId, views, widgets, twin}`.
60
- Reads the unique hidden Device render definition, preserving exact Widget/Twin
61
- bindings. Disabled devices can have empty definitions. Multiple anchors fail rather
62
- than choosing an arbitrary visible Card. Widgets contain configuration, never values. The optional `twin` metadata is
63
- returned even when a known device has no render anchor.
64
- - `/app/device/history/series/get`, `{deviceId, capabilityIds?: []}`:
65
- `{deviceId, series}` with per-capability decoded `states`, `events`, `logs`. Empty
66
- capability selection reads all series. This preserves chart structure; the shared
67
- Device history Facade remains a bounded, merged business timeline.
64
+ Device-specific rendering, chart series and selected online reads use the
65
+ [`/app/presentation/device/*` contract](device-presentation-v1.md). They are UI
66
+ adapters, separate from the shared definition/state/history contracts.
67
+
68
68
  - `/app/dashboard/canvas/content/get`, `{canvasId}`: Canvas metadata plus Cards' static
69
69
  titles, view bindings, layouts, settings, metadata and selected Widget definitions.
70
70
  Each entity Card includes the same `twin` metadata, loaded once per entity in the
@@ -88,7 +88,7 @@ identity/connection/capabilities. Schema publication does not expire them. Expli
88
88
  device refresh rereads attributes and reconciles related Groups. These are write-side
89
89
  operations; definition reads remain cache-only.
90
90
 
91
- `/twin/entityUpdate` pushes `{scopeId, entityIds}` after cached metadata changes.
91
+ `/twin/entityUpdate` pushes `{spaceId, entityIds}` after cached metadata changes.
92
92
  Open views refresh their definition batches; other Spaces and unrelated entities
93
93
  are ignored. A new definition owns its capability membership: old observations
94
94
  for removed capabilities must not be merged back into the current view. History
@@ -0,0 +1,17 @@
1
+ # Linear execution attribution
2
+
3
+ Trace is an ordered array of flat items, each with an uppercase type and Unix-millisecond timestamp. Source kinds reuse Facade source kinds. The array records user-visible attribution boundaries; Automation instructions, conditions, branches and payloads are not trace items.
4
+
5
+ Ignite explicitly starts an Automation, trigger starts an ordinary run, and resume continues an existing run. Resume preserves the saved run chain. Internal asynchronous instruction completions do not append resume items. Trace uses run-level attribution, without per-branch causal graphs.
6
+
7
+ Command history stores an optional `r` reference. Snapshots are immutable and scoped by tenant and Space; IDs are positive signed 64-bit integers, represented as decimal strings in JSON. The trace and referencing command use the existing command storage owner. Recording is best effort: missing attribution cannot fail an operation or suppress its command history.
8
+
9
+ Each cross-runtime invocation keeps its own frozen attribution prefix in the caller's in-memory context. The provider returns only the local suffix for each command it recorded; the caller composes each suffix with that invocation's prefix independently. Responses may complete in any order. Different calls do not share a mutable trace, and no shared placeholder list, durable pending record or cross-runtime command ID is required. Closing a request discards later recording work under the existing best-effort bounds.
10
+
11
+ Trace collection does not change Twin observation semantics. Reports and observed state values update Twin when received; unchanged state retains its transition time and existing deduplication, while events represent separate occurrences. Received command records update the latest command log and append history under the existing rules. A late response is not rejected merely because another command started later. A command log is distinct from reported device state and does not establish that a physical state change occurred.
12
+
13
+ `/app/trace/get` accepts `input.traceIds`, at most 100 decimal IDs in the current Space. The result contains `traces` (ID to array) and `missingTraceIds`. Normal history reads do not implicitly resolve references. Entity and device history timelines return the command’s optional `r` unchanged as a decimal string; state and event entries omit it. Their `traceSteps` contains only legacy stored string sources, never a downgraded projection of a structured snapshot. Callers that need attribution batch the selected references through `/app/trace/get`; missing or expired snapshots leave the command visible. Full returns the original structured model with Unix-millisecond timestamps; Essential preserves its attribution fields and renders item times in the Space Home timezone. Authorization is Space read and routing follows Space mode. Legacy stored string sources remain readable without inventing per-item times.
14
+
15
+ Snapshots have a seven-day best-effort retention window. Local cleanup is opportunistic on writes; cloud providers use native TTL. Reuse is a small in-memory optimization, without a global deduplication guarantee.
16
+
17
+ Attribution is bounded to 256 items and 32 KiB encoded JSON. Oversized attribution is omitted. Consecutive resumes of the same Automation keep the latest resume timestamp in the new snapshot, preserving the original chain and previously persisted snapshots.
@@ -0,0 +1,47 @@
1
+ # Personal home navigation
2
+
3
+ The signed-in User owns two independent profiles, `compact` and `wide`, shared
4
+ across Web, desktop and phone Clients and independent of the focused Space.
5
+ `/app/user/navigation/catalog` takes an empty object and returns the canonical
6
+ `manifest/navigation-cards.v1.json` catalog. `get` takes `{layout}`. `update`
7
+ takes `{layout, cardIds, menuCardIds?}`. Both return
8
+ `{layout, cardIds, menuCardIds, revision}`. The authenticated caller selects
9
+ the owner; requests cannot select a User, Client, Space, URL or arbitrary page.
10
+
11
+ `cardIds` is the ordered Nav list. Dashboard, Now and Menu each occur exactly
12
+ once; Menu is always projected and stored last. Requests and stored profiles
13
+ from older Clients with Menu elsewhere are normalized, preserving all other
14
+ relative order. Additional entries are Menu card IDs from the catalog.
15
+ Membership makes the card's root route a home page; child editors and object
16
+ details retain their ordinary navigation lifecycle.
17
+
18
+ `menuCardIds` is the ordered list of remaining Menu cards. A complete edit supplies
19
+ both lists and replaces them in one document write. They must be disjoint and
20
+ together contain every catalog ID exactly once. Required Nav entries cannot move
21
+ to Menu. Unavailable cards remain in their saved lists/order when a Client filters
22
+ its UI, so a different Space/platform does not delete personal preferences.
23
+ New catalog cards absent from an older request or stored profile are appended in
24
+ default order. Unknown IDs, duplicates and overlap are rejected.
25
+
26
+ For backward compatibility, omitting `menuCardIds` changes only Nav membership
27
+ and order: retained Menu entries keep their saved relative order, newly unpinned
28
+ entries are appended, and newly pinned entries leave Menu. Older readers may
29
+ ignore the additive response field. The default Nav list is
30
+ `[dashboard, now, menu]`; the default Menu order is Devices, Automations, Widgets,
31
+ Integrations, Messaging Apps, NFC, Matter, Camera, Floor Plan, People, Storage,
32
+ Phone Setting. No startup data rewrite or separate Menu document is needed.
33
+
34
+ UI edits are drafts until the user clicks Done. Normal browsing does not reorder
35
+ or show per-card editing menus. Editing blocks page navigation, supports transfers
36
+ between Menu and Nav and sorting within both, and leaves Menu fixed last with a
37
+ thin separator. Dropping outside both lists changes nothing; touch scrolling
38
+ stays native until a held drag activates. Cancel discards the draft; a failed
39
+ save retains it for retry. Screen profiles remain independent.
40
+
41
+ Writes are last-write-wins per profile, without a caller revision precondition.
42
+ An opaque revision identifies a saved snapshot, not a Client clock. Each write
43
+ replaces one profile only. `/user/navigation/changed` invalidates that authenticated
44
+ user's profile across their connected Clients. Consumers re-read authoritative
45
+ state and also resync on reconnection/focus; notifications are not a second durable
46
+ store. Saving a profile does not require an App Client online and never performs
47
+ a page's business action.