Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

- [App shell and views](./docs/app-shell.md): typed view registry, default view,
selector contracts, and how to add new app views.
- [Background operations and app notifications](./docs/background-operations-and-notifications.md):
non-blocking Effection workflows, operation history, resource conflicts,
notification UX, and how to add new background flows.
- [Local persistence](./docs/persistence.md): controller-AID-keyed local
storage for operations and app notifications, reload interruption semantics,
and maintainer change checklist.
- [Identifier UI](./docs/identifier-ui.md): identifier list/details display
rules, AID truncation/copy behavior, KIDX/PIDX/tier extraction, and rotate
affordances.
- [Signify client boundary](./docs/signify-client-boundary.md): ownership,
configuration, and public boundary API.
- [Runtime config](./docs/runtime-config.md): app/runtime config ownership,
Expand Down
33 changes: 23 additions & 10 deletions docs/app-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Feature UI remains under `src/features/*`. Feature components render loader and
action state; they do not construct Signify clients or own route registration.
For the Effection, service, workflow, and Redux state layers behind
`AppRuntime`, see [Workflow and state architecture](./workflow-state-architecture.md).
For the background operation indicator and notification bell, see
[Background operations and app notifications](./background-operations-and-notifications.md).

## Runtime Boundary

Expand Down Expand Up @@ -118,8 +120,8 @@ fields are `routeId`, `label`, `gate`, `nav`, and `testId`.

`AppRouteId`

: Closed set of current feature route IDs: `identifiers`, `credentials`, and
`client`.
: Closed set of current feature route IDs: `identifiers`, `credentials`,
`client`, `operations`, and `appNotifications`.

`AppRouteGate`

Expand All @@ -132,13 +134,19 @@ their handles.

## Current Routes

| Path | Route behavior | Loader/action owner | Gating |
| -------------- | ------------------------------ | -------------------------- | --------------------------------- |
| `/` | redirects to `/identifiers` | root child index loader | none |
| `/identifiers` | identifier list/detail/create | identifiers loader/action | connected Signify client required |
| `/credentials` | connected placeholder | credentials loader | connected Signify client required |
| `/client` | client/controller/agent state | client loader | connected Signify state required |
| `*` | redirects to `/identifiers` | catch-all loader | none |
| Path | Route behavior | Loader/action owner | Gating |
|--------------------------|----------------------------------------|---------------------------|-----------------------------------|
| `/` | redirects to `/identifiers` | root child index loader | none |
| `/identifiers` | identifier list/detail/create/rotate | identifiers loader/action | connected Signify client required |
| `/credentials` | connected placeholder | credentials loader | connected Signify client required |
| `/client` | client/controller/agent state | client loader | connected Signify state required |
| `/operations` | persisted/background operation history | Redux selectors | none |
| `/operations/:requestId` | operation detail and result links | Redux selectors | none |
| `/notifications` | app-level user notifications | Redux selectors | none |
| `*` | redirects to `/identifiers` | catch-all loader | none |

Operations and app-notification routes are intentionally ungated. Persisted
history should remain viewable after disconnect, refresh, or reconnect.

Direct navigation to a gated route renders `ConnectionRequired` until the user
connects. Routes do not auto-open the connect dialog.
Expand Down Expand Up @@ -206,6 +214,9 @@ needs.
handled by `RouteErrorBoundary`.
- Mutations that affect a route's loader data should live in that route's
action so React Router revalidation stays predictable.
- Background mutations return accepted/conflict action data immediately. They
should update visible route state through Redux completion facts rather than
relying only on route revalidation.
- Future credential actions should use explicit intents such as `issue`,
`grant`, `admit`, and `present`, not a generic command string.

Expand Down Expand Up @@ -246,7 +257,9 @@ route descriptor and handle, not editing a second navigation registry.
`TopBar`

: Shell-only app bar with the stable `nav-open` and `connect-open` smoke-test
selectors.
selectors. It also renders the active-operation indicator and app notification
bell from Redux selectors. It must not know Signify clients or route loader
internals.

## Adding A Route

Expand Down
140 changes: 140 additions & 0 deletions docs/background-operations-and-notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Background Operations And App Notifications

This document explains the app's non-blocking operation model, app-level
notifications, operation history routes, and shell indicators. Use it when
moving KERIA work out of a blocking route action or when adding user-visible
completion/failure feedback for long-running work.

## Mental Model

The app has two workflow launch paths:

| Path | Runtime API | UX contract | Examples |
| --- | --- | --- | --- |
| Foreground | `AppRuntime.runWorkflow(...)` | The user is blocked by route loading, a fetcher submission, or the global loading overlay until work finishes. | Connect, passcode generation, route loader reads. |
| Background | `AppRuntime.startBackgroundWorkflow(...)` | The route action returns immediately with accepted/conflict metadata while Effection watches the KERIA work in the session scope. | Identifier create and rotate. |

Foreground work is still correct when the app cannot proceed without the
result. Background work is correct when the user can keep navigating while the
operation finishes and can inspect progress or completion later.

Top-level background handoff belongs in `AppRuntime`. Use Effection `spawn`
inside a workflow only for true child concurrency within one unit of work. Do
not start top-level background tasks directly from React components or services;
that bypasses operation tracking, conflict checks, and notifications.

```mermaid
flowchart TD
Action["Route action"] --> Runtime["AppRuntime.startBackgroundWorkflow"]
Runtime --> Conflict["resourceKeys conflict check"]
Conflict -->|conflict| ActionData["typed conflict action data"]
Conflict -->|accepted| Operations["operationStarted record"]
Operations --> Task["Effection task in session scope"]
Task --> Workflow["workflow/service/KERIA wait"]
Workflow --> DomainState["domain Redux state"]
Task --> Completion["operation success/error/cancel"]
Completion --> Notification["app notification"]
Completion --> Routes["/operations and /notifications"]
```

## Operation Records

`src/state/operations.slice.ts` stores serializable operation facts. These
records power active-operation indicators, operation history, detail pages,
conflict guards, and persistence.

Important fields:

| Field | Purpose |
| --- | --- |
| `requestId` | Correlation key shared by route action response, operation detail route, and app notification. |
| `kind` | Machine-readable operation category. Add a typed kind before wiring new workflow families. |
| `phase` | Human-readable current phase. Today most flows use lifecycle status; future workflows can update finer phases. |
| `resourceKeys` | Optimistic concurrency keys used to reject or disable conflicting work. |
| `operationRoute` | Link to `/operations/:requestId`. |
| `resultRoute` | Optional link to the operation-specific success context. |
| `notificationId` | Back-reference to the app notification created for background completion/failure. |
| `keriaOperationName` | Optional raw KERIA operation name for diagnostics, never the raw operation object. |

Redux must stay serializable. Do not store `Task`, `SignifyClient`, raw KERIA
operation responses, `Error`, `AbortController`, DOM nodes, or Promise objects
in any state slice.

## Conflict Guarding

Each background operation should declare resource keys before launch. The
runtime rejects a new background task when a running operation owns any of the
same keys.

Current keys:

- Identifier create: `identifier:name:<name>`
- Identifier rotate: `identifier:aid:<aid-or-alias>`

Expected future keys:

- Contact resolution: `contact:<alias>`
- Schema resolution: `schema:<said>`
- Registry creation: `registry:issuer:<aid>`
- Credential flows: `credential:<said>`

The UI should disable only the conflicting action, not the whole app. For
example, rotating one identifier should disable only that identifier's rotate
button while allowing navigation and other non-conflicting actions.

## App Notifications

`src/state/appNotifications.slice.ts` is the user-facing notification system for
app work. It is separate from `src/state/notifications.slice.ts`, which tracks
KERIA notification inventory and processing status.

An app notification is created by `AppRuntime.recordCompletionNotification(...)`
when a background task completes successfully or fails and the launch options
provide a notification template.

Notification links:

- `operation`: required link to `/operations/:requestId`.
- `result`: optional link to the operation-specific result route.

Notification read behavior:

- The bell popover in `TopBar` marks visible unread notifications as read after
1250 ms.
- The `/notifications` route also marks unread notifications as read after
1250 ms.
- `selectAppNotifications` returns notifications in descending `createdAt`
order.

## Shell And Routes

The app shell exposes background work through:

- `TopBar` operation indicator: active count and popover of running operations.
- `TopBar` notification bell: unread count and recent app notifications.
- `/operations`: reverse-chronological operation history.
- `/operations/:requestId`: operation detail with lifecycle fields and links.
- `/notifications`: app notification list.

Operations and notifications routes have no connection gate. Persisted history
must remain visible after disconnect or refresh.

The global `LoadingOverlay` is for foreground work only: connect, passcode
generation, route navigation, and loader/fetcher pending state. Background
operations must not trigger the blocking overlay.

## Adding A Background Flow

1. Add a typed `OperationKind`.
2. Define resource keys before launch.
3. Add or extend an Effection service operation for Signify/KERIA calls.
4. Add a workflow that composes services and dispatches domain Redux state.
5. Add `AppRuntime.startX(...)` using `startBackgroundWorkflow(...)`.
6. Provide title, description, result route, and success/failure notification
templates.
7. Wire the route action to return accepted/conflict metadata immediately.
8. Use selectors to disable only conflicting UI actions.
9. Add reducer/runtime/route-data tests and a smoke check for the visible UX.

Do not add a background path without a conflict key unless the operation truly
cannot conflict with any other valid user action.
135 changes: 135 additions & 0 deletions docs/identifier-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Identifier UI

This document explains the identifier list and details UI. Use it when changing
identifier display fields, rotation affordances, or Signify `HabState`
presentation helpers.

## Data Model

The app's identifier display model is `IdentifierSummary`, currently an alias
for Signify's `HabState`.

Important display fields:

| UI field | Source | Notes |
|-------------|-------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
| AID | `identifier.prefix` | Long values are shortened in the table, full value remains copyable. |
| Current Key | `identifier.state.k[0]` | The first current public key. Multiple keys are indicated in details and fully visible in Advanced JSON. |
| KIDX | `identifier.salty.kidx` | Exposed by salty identifiers. Do not derive it for other identifier types. |
| PIDX | `identifier.salty.pidx` | Exposed by salty identifiers. Do not derive it for other identifier types. |
| Tier | `identifier.salty.tier` | Exposed by salty identifiers. |
| Type | Tagged `HabState` branch: `salty`, `randy`, `group`, or `extern`. | Use shared helper extraction. |

Randy, group, and extern identifiers do not currently expose the same local
key-manager metadata as salty identifiers. The UI should show an unavailable
placeholder for missing KIDX, PIDX, and tier values rather than deriving
misleading values from event sequence numbers.

## Helper Ownership

`src/features/identifiers/identifierHelpers.ts` owns identifier display
extraction:

- `identifierType(...)`
- `identifierCurrentKeys(...)`
- `identifierCurrentKey(...)`
- `identifierKeyIndex(...)`
- `identifierIdentifierIndex(...)`
- `identifierTier(...)`
- `formatIdentifierMetadata(...)`
- `identifierJson(...)`
- `truncateMiddle(...)`

Components should not duplicate Signify shape guessing. If a new identifier
branch exposes new display metadata, add it to the helpers and unit tests first,
then render it from components.

## Details Surface

`IdentifierDetailsModal` is the current identifier detail surface. There is no
dedicated `/identifiers/:id` route in this phase.

The modal shows human-readable fields first:

- Name
- AID
- Type
- Current Key
- Key Index
- Identifier Index
- Tier

Full identifier JSON is hidden behind the `Advanced JSON` accordion. The JSON
highlighter is local and React-rendered. It must not use
`dangerouslySetInnerHTML`.

The details modal keeps a rotate action, but route/runtime ownership remains in
`IdentifiersView`. The modal receives `onRotate`; it never calls Signify or the
runtime directly.

## Identifier Table

The desktop table columns are:

- Name
- AID
- Current Key
- Type
- KIDX
- PIDX
- Actions

Mobile cards mirror the same core fields and rotate action.

The AID and current-key display contract:

- use `truncateMiddle(aid)` as first eight characters, `...`, last eight
characters for long values,
- show the full value in a tooltip,
- copy the full value on click,
- stop click propagation so copying does not open details,
- use the shared `--app-mono-font` CSS variable.

The table rotate button:

- uses the rotate icon,
- stops click propagation so rotating does not open details,
- calls the parent `onRotate`,
- is disabled only for the row whose active operation owns
`identifier:aid:<name>`.

## Rotation And Fresh State

Identifier rotation is a background operation. `IdentifiersView` submits the
route action, the runtime starts the Effection workflow, and the workflow
refreshes identifier Redux state after KERIA completion.

Successful rotation must refresh local AID state so fields such as KIDX update
without requiring a manual route reload. If Signify/KERIA response shapes change
and include a fresh `HabState`, update the service layer and tests before
changing UI behavior.

## Font

`src/index.css` imports Source Code Pro from Google Fonts and defines:

```css
--app-mono-font: 'Source Code Pro', 'Roboto Mono', 'SFMono-Regular',
Consolas, 'Liberation Mono', monospace;
```

Use this variable for AIDs, current keys, and JSON/code-like identifier data.
Do not introduce one-off monospace stacks in components.

## Tests

Relevant coverage:

- `tests/unit/identifierHelpers.test.ts` covers salty and randy metadata
extraction, unavailable values, JSON formatting, and middle truncation.
- `tests/browser-smoke.mjs` checks that the table includes AID, KIDX, PIDX, and
Actions headers after connect.
- Scenario tests protect real Signify/KERIA rotation behavior.

Run `pnpm unit:test`, `pnpm browser:smoke`, and `pnpm scenario:test` when
changing display helpers or rotation behavior.
Loading
Loading