Problem and update settings
Problem reporting Settings row
Settings → System renders the admin-only ProblemReportingRows (web/modules/settings/SystemSettingsSection.tsx:211). The component returns nothing for a non-admin (web/modules/settings/ProblemReportingRows.tsx:60). The switch opens the shared ConfirmDialog
before enabling (ProblemReportingRows.tsx:74-80). Its formal cs/sk/en explanation lists the reported fields, including up to three
redacted error message samples per problem and what redaction removes, identifiable licence,
recipient, 14-day remote retention and disable semantics (web/lib/i18n/dictionaries/en.ts:199-203). The final config write requires explicit
confirmation and a config revision on the server (ProblemReportingRows.tsx:65, which sends problemReportingConfirmation: true with config.data.revision). A separate shared inspect rail (WorkspaceDetailRail) shows the last already-sent
batch; it is not prior payload approval (ProblemReportingRows.tsx:81). The SettingsRow control is the switch, its short status is
only the delivery state, and the full notice is passed as the row's hint (ProblemReportingRows.tsx:72-76). The inspect rail
owns the recipient link, queued/dropped counts and last delivery time so none of these long details
occupy the row's single control band (ProblemReportingRows.tsx:84-100). Each batch entry lists its redacted samples as wrapping monospace
lines (ProblemReportingRows.tsx:45-47). Loading/error states use the host components in SettingsState
beside the row (ProblemReportingRows.tsx:77-78); the inspect rail uses the same states plus EmptyState for an unsent batch (ProblemReportingRows.tsx:82-83, :101).
The licensing plugin owns an Errors section through its existing SectionDeck and host runtime.
Shared toolbar, card and state components render validated envelopes only. The list has one server-side
free-text search (customer, licence, instance id prefix, code, plugin, localized description, sample text,
level, status) and a sort choice in the shared Filters panel; the detail shows the plain-language
description with the code, the redacted message samples, paginated instance rows and an audited
acknowledgement. Acknowledged means seen, not fixed. Browser verification belongs to the actual host flow.
The Errors list's Filters button holds a destructive "delete all error reports" action too, so the plugin
hands it to the shared toolbar as filterActions (web/components/ui/PageToolbar.tsx:145, :163) and the host names the button "Options". The confirmation
is the shared ConfirmDialog, owned by the plugin outside the panel. The deck's sections and the main
navigation entry wear counts: DeckNavItem.badge for Errors (open problems), Releases (core releases),
Licences and Customers, and the plugin's registerNavBadge for the open problems on the Licensing entry.
The status/report schemas use the existing browser-mirror boundary in web/lib/problemReporting.ts;
webMirrorContracts.test.ts pins the marked schemas and recipient URL to their core sources,
ignoring only export keywords (tests/contract/webMirrorContracts.test.ts). The licence contract remains the sole daemon-side URL authority.
No daemon runtime import reaches Next.
The web application is a Next.js App Router application in web/. It is rendered per request because authentication, branding, locale, skins, plugin availability, and navigation depend on current state. The root layout provides the shell, localization, branding, React Query, event handling, overlays, toasts, and advisor surfaces.
Marketplace update status
elowenClient.installPlugin, updatePlugin, repairPlugin and systemUpdate receive
a queued acknowledgement { requestId, queued: true } (web/lib/elowenClient.ts:314, :415-426). This acknowledges a durable request, not installed files,
enablement or successful startup. Install/update still answer consent 409 with the daemon's grant
list; usePluginConsent replays the same operation with exactly those acknowledgeGrants (web/modules/settings/usePluginConsent.tsx:96-99, :101-110).
Candidate control dependency errors retain their existing human explanation (usePluginConsent.tsx:40-61).
The same pluginChangeRefusalError mapper in usePluginConsent handles HTTP 409
update in progress with a request id for toggle and removal toasts, using cs/sk/en copy rather
than raw updater text (usePluginConsent.tsx:42-44). No second error dialog or client-side mutation guard is introduced.
useSystemUpdateStatus in web/lib/queries.ts owns the single shared system-update query
for admin GET /system/update (queries.ts:206-226). Its wire DTOs are re-exported by web/lib/types.ts from the core contract, covering the normalized queue,
journal summary including coreRequest, and UpdateReport with run request ids and per-plugin outcomes. Accepted mutations
seed a queued hint in that cache and invalidate only status, never optimistically install or enable a
plugin (web/lib/mutations.ts:52-54). All cards and the System panel share the read; there is no request or poll per plugin.
It polls once per second while the queue or journal exists, stops when settled or needing operator
attention, and refreshes on focus (queries.ts:222-224). A new terminal report invalidates installed/detail/marketplace,
configuration, command, plugin UI, conversation-link and system-version views (queries.ts:214-218).
The shell's existing useElowenEvents stream cancels the outstanding system-update
or memory-maintenance query before invalidating it on its core event and stream open.
Plain invalidation reuses a pending first read with no cached data, so a pre-event idle
answer could otherwise hide newly started work and leave active-only polling stopped.
Cancellation retires that answer through React Query; the shared enabled observers
immediately read fresh status, while disabled or absent observers do not fetch. No
second event stream, idle polling, retry, or feature-local cache is introduced. Memory
events still refresh the memory list and vitality through their existing keys.
src/shared/updateStatus.ts::pluginUpdateRecords(status, name, requestId?) selects the matching
queue, journal and report records for both web and setup (src/shared/updateStatus.ts:5). It scans each retained list by plugin
name and optional request id, returns undefined for absent records, and makes no completion decision.
The web cannot import daemon runtime, so web/lib/updateRecords.ts carries the same body below its
own type import, pinned byte-identical by tests/contract/webMirrorContracts.test.ts.
web/lib/updateStatus.ts projects those records as queue, active journal and terminal outcomes in that order (web/lib/updateStatus.ts:19-35),
retaining request ids so an older result cannot stand in for a newer request. Applied, rolled back
with detail, needs consent, not licensed, superseded, failed, skipped and operator attention are
distinct. Only System renders UpdateStatusPanel, with existing settings rows and shared
loading/error/empty states (web/modules/settings/UpdateStatus.tsx:20). The Installed plugin list has no separate update-status card, even when
idle. Each plugin row keeps its UpdateProgressBadge for queued, active and terminal states (UpdateStatus.tsx:9). An explicit
update already satisfied by the unchanged installed receipt and approved pin reports applied, so
the existing success badge and setup waiter both accept it; downgrades remain failed.
Update availability remains the marketplace catalog's comparison of the offered version with the
installed manifest; badges remain the coordinator's recorded verdicts. Both refresh after a new
terminal report. An existing incorrect report is replaced by the next real transaction, never
rewritten or overridden by presentation code.
Settings Plugins names its shared filter/action panel Actions and keeps the category picker and
active chips (en.ts:352; web/modules/settings/PluginsSection.tsx:422). Update all plugins opens the shared restart confirmation, then sends one
POST /plugins/marketplace/update (elowenClient.ts:423). The shared consent hook collects any required grants by plugin
and retries the whole batch; a refusal queues nothing. useUpdateAllPlugins seeds all accepted
operations in the same status cache, without a request or status query per plugin (web/lib/mutations.ts:379). The action is
disabled while nothing is offered or a request/run is pending. Disabled plugins stay disabled. Rolled
back, failed, needs-consent, not-licensed and superseded reasons appear below the plugin description
through the same span and classes as the degraded reason; distinct update and load reasons are both
retained, identical reasons appear once. Every progress badge carries its detail in its native title.
No detail means no reason line or title. Run-level report badges never borrow an individual request id;
plugin badges carry their own report outcome and request id. The committed run label is distinct
from the applied plugin label in cs/sk/en. React Activity retains inactive settings panels, so
browser assertions scope to the visible status group. A failed status read offers Retry, never a
guessed success.
Only the server's last report is retained; this surface is not an update-history store. An accepted
request id absent from queue, active journal and report is terminal superseded, including ids
removed by normalization or disable/uninstall. A different request's outcome is never success for it
(web/lib/updateStatus.ts:32-33).
A receipted unloadable plugin remains Installed with degraded.installedVersion and
degraded.reason, a visible load-failure explanation and Repair. Repair queues the same coordinator
transaction. Degraded state blocks enabling, not disabling: an enabled degraded plugin remains
switchable off when no active request makes it busy. Active requests disable duplicate install, update and repair controls. System Update now
uses the same query rather than declaring completion from its POST acknowledgement. Every state has
Czech, Slovak and English copy. web/tests/e2e/specs/settings.updates.e2e.ts drives queued install,
applied proof, consent replay, rollback reason, degraded repair and System status at 320 and 1440 px
in all three locales, including empty, delayed-loading and failed-status retry, with behavioural and containment assertions instead of pixel baselines.
Its Czech capture matrix also checks the correlated HTTP 409 toggle refusal and retained enablement,
then records the plugin list at both widths through the existing full-height design harness.
Repair controls belong to the plugin list, not the System run-status panel.
MCP server settings and OAuth
The bundled MCP settings page uses the plugin API through ElowenUiRuntime.api.
GET /plugins/mcp/api/servers supplies server descriptions and authentication states (plugins/mcp/cliPicker.mjs:13; the same route is used by the browser page).
Sign-in and sign-out POST paths use URL-encoded server names and the selected personal or instance
ownership scope (plugins/mcp/cliPicker.mjs:69 for the sign-in path). Sign-in reserves a tab during the user gesture, removes its opener, and navigates it
only after validating the returned authorizationUrl (plugins/mcp/web-src/McpServersPage.tsx:546-548). Returning focus to Elowen refreshes server state
without discarding the editor draft. Opening the authorization page never implies authentication success.
Without an authorization requirement, no sign-in action is shown. Stored SSE servers show an unsupported
transport state and cannot connect; the editor requires an explicit switch to Streamable HTTP (plugins/mcp/elowen-plugin.json:88; plugins/mcp/docs/mcp-plugin.md:23).
MCP OAuth returns to /p/mcp/oauth/callback on the trusted instance public origin (plugins/mcp/elowen-plugin.json:44; plugins/mcp/docs/mcp-plugin.md:37). The page posts
{ state, code?, error?, iss? } once to /api/plugins/mcp/api/oauth/callback through the existing
authenticated browser proxy and immediately strips code and iss from its URL. The daemon validates
iss as an optional string and forwards it unchanged to PI's RFC 9207 issuer check. A mismatched issuer,
or a missing issuer when required, fails closed before token exchange. The daemon returns HTTP 200
{ server } or a 4xx response with expired, invalid_state, denied or failed. Every known state
is consumed on every outcome, including a wrong account. There is no GET callback and no daemon redirect. The only public route the plugin publishes for OAuth is the client metadata document at /hooks/mcp/oauth/client-metadata, used where the server needs a metadata-document client ID (plugins/mcp/docs/mcp-plugin.md:29). The page shows loading and bounded failure states and returns to MCP after success;
it does not automatically retry an ambiguous write. Authentication performs bounded network requests,
not model inference; secret storage and cross-process refresh locking are described in
External MCP bridge.