Account defaults and the Account page
Every defaultable personal setting resolves through one chain: the account's own stored value, else its
account preset under sentinel -presetId, else the instance's account default under sentinel 0, else
the code default. Presets exclude the instance conversation-model role and personal plugin settings.
The daemon owns that chain and the routes that read and write it; this page is the browser half.
Directory month spend
GET /users carries monthSpentMicrousd: number | null on each UserDirectoryRow,
including chatbot accounts. AccountSpendGate.directory() reads the directory's saved baselines
and one grouped usage.providers.spentByUser(fromDay, toDayExclusive) aggregate in a repeatable-read
snapshot. The UTC month bounds and accountMonthSpentMicrousd calculation are shared with
headroom(), the source of GET /users/:id/spend: round USD to micro-USD once, subtract
only this month's administrator reset baseline, and clamp to zero. The read is two queries,
not one spend request per account; baselines never leave the server.
An account whose usage accounting is latched unhealthy (a failed usage write, until restart) reports
null rather than a misleading zero; a failed database read fails the directory request as before. UsersView renders the
existing spendLimit.spent label below web activity (below creation for chatbots), using the
drawer's two-decimal money format. Null renders a dash with the existing read-error tooltip.
Resetting monthly spend invalidates both the drawer reading and the directory query.
Administration project lists
The Users register and account-preset editor call useProjects('assignable'), backed by
GET /projects?view=assignable. Core filters this list through the membership repository's canManage;
a shared computer owned by another account therefore remains available for execution without appearing
in administration pickers. GET /users/:id/projects returns only assignments the caller may manage,
so the drawer's Project count and rows use the same bounded set. Query keys separate this administration
projection from the ordinary Project register and invalidate together after assignments change.
The defaults window
web/modules/users/AccountDefaultsWindow.tsx is what the Users page opens from the toolbar's Options
panel (UsersView passes filterActions to ControlSurfaceToolbar, so the panel's trigger and sheet
are named "Options"). It is drawn in the shared PageOverlay reading frame, the same window Settings and
Account are read in (icon tile and title only), around a SectionDeck whose content is a SettingsPage
hero — eyebrow, the open section's own name and the intro as the line under it — over that section's panel.
Its column is the shared DeckNavigation with the same searchable field and group caption Account's has:
the search filters the sections by name (diacritics-insensitive; there is no record index for defaults) and
says users.defaults.navNoMatches when none match; the phone strip has no search, as everywhere. Eight sections: Models,
Memory, Skills, Style and instructions, Language, Rules and YOLO, Plugins and the new-account preset. The window is
opened from a button rather than addressed, so it passes onClose (Escape or the header close control, which
is blocked while the autosave is writing) instead of the history step; there is no separate Done button. The
default language is the shared ChoiceField dropdown. The "Apply to everyone" confirmation and the
selection pickers open above it.
The window reads GET /users/defaults once and keeps its own draft of settings and
newAccountPresetId; its autosave diffs that draft against the view it was seeded from and PATCHes only
the fields that moved, with the revision it was read at. A draft that moved on while a write was in
flight is the reader's and is never overwritten by the answer, and the value the daemon ANSWERS for a
field that was written is adopted, so a canonicalized value cannot read as a further difference and
bounce back. A stale revision is a 409: the window keeps the draft and offers a reload, which is the one
answer that cannot lose a newer write.
The new-account control is a ChoiceField selecting no preset or one named preset. The configured
newAccountPresetId seeds human accounts created through Users and Microsoft SSO; an explicit no-preset
choice in the creation form starts the account closed. The first administrator keeps its bootstrap
full grant and chatbot accounts keep their fixed grant.
Every record states how many human accounts hold their own value for its field, and carries the action
that clears them (POST /users/defaults/apply). That action's ConfirmDialog is rendered BESIDE the
dialog rather than inside it, because the deck under it is the scroller and a question must not be
reachable only by scrolling back to the control that asked it.
web/modules/users/AccountDefaultsPlugins.tsx renders each plugin's default per-account form with the
same PluginConfigEditor and usePluginConfigDraft the account page uses, bound to the defaults route
through the draft's save option; secret fields have no default and are absent from the schema the
daemon sends there. The save adapter maps the route's values to the shared draft's canonical config; a stale plugin revision includes current: { config, secretsSet: [], revision }, so Reload and Merge work through the same conflict handling.
Account presets
Users opens the account-presets window from Options beside the defaults action. The window uses
PageOverlay, SectionDeck and searchable DeckNavigation for the preset list and the new-preset
entry. Each preset edits its name, grants, behavior and minimum Project memberships. Grant editors
share the account detail's presentation controls in GrantSelections, ToolSelection,
VoicePermissionRows, ProjectPermissionRows and ProjectSelection. Their containers own mutations;
presentation takes a value and onChange. Behavior uses AccountBehaviorPanel for the defaults rows
and their apply-to-everyone confirmation, scoped to attached human accounts. Its preset target exposes
stored-field inheritance actions and hides the conversation role through ModelRoleRows.hideChatModel.
SpendLimitSlider reports a committed microusd value or null; every stop is a storable limit, so the
preset window has no invalid spend state, and the account container commits the value through its
existing mutation.
The browser uses web/lib/accountPresets.ts for the admin-only /users/presets DTO. A preset has
one optimistic revision and reports its attached-account count, effective settings, stored fields
and attached accounts' own-value counts. A null behavior value clears that preset field to inheritance.
The conversation-model role and personal plugin settings remain in the instance defaults window.
The account detail drawer (web/modules/users/UserDrawer.tsx) and human-account creation select a preset
with ChoiceField. Before the first manual grant edit, the drawer's GrantChangeGate (keyed by the preset
id) confirms detachment from the named preset; the server detaches atomically with that edit and reports
presetDetached. Personal /auth/me/* saves preserve attachment. The account row and the drawer's header
meta line show quiet role and preset metadata, with the same text treatment for chatbot accounts.
Deleting a preset confirms its account count, detaches those accounts and preserves their grant
columns, own settings and memberships. They subsequently inherit instance behavior for fields without
a personal value.
Shared editors
The rows, pickers and cards the window edits are the account page's own, taken from
web/modules/account/editors/: ModelRoleRows, MemoryAutomationRows, MemoryMaintenanceRows,
PermissionRulesEditor with PermissionRulesPreview and StyleAndInstructionsRows. Each takes a value and hands back a change; the containers
(CliSection, AccountMemorySection, MemoryDreamingCard, PersonalitySection,
PermissionRulesCard) keep the reads, the seeds and the writes. A second surface editing the same
records therefore cannot drift on what a switch is called or which models a role may pick.
The Account page sections
web/modules/account/sections.ts is the one list of what the Account page is made of, addressed as
/account?cat=<id> (accountSectionHref). CORE_ACCOUNT_SECTIONS holds the core ids, and accountSections
draws core and plugin sections in ONE sort by a numeric order: profile 100, plugin panels without a manifest
order 200, Sandbox's Desktops 250, elowenCli 260 (labelled Computers; not cli, which is Models), plugin
configuration forms 300, then cli 400, voice 500, memory 600, personality 700, notifications 800, security 900,
apiTokens 1000 and terminal 1100. Equal orders put core first, then plugin name, then section id, compared by
code point, so the menu is identical on every load. A plugin panel's order is its manifest web.account[].order
(integer), carried through GET /plugins/ui unchanged. The caller passes plugin contributions in rather than the module
resolving them: pluginAccountSectionEntries reads the plugin listing, and userPluginConfigSectionEntries adds the
per-account configuration forms of plugins that do not place them on their own page. A plugin section with
placement: 'linkedAccount' opens in the profile's Linked accounts drawer instead of claiming a section.
isAccountSection validates a stored or linked id by shape only; whether a plugin's section still exists is
checked against the listing separately.
Account > Security and two-step verification
The existing AccountPanel id="security" owns the password form followed by
AccountTotpSection and LoginSessionsSection; no new route, navigation entry or account
section exists. Unlike ordinary drafts retained by React Activity, the TOTP subtree unmounts on
every section exit. The one-time enrollment response lives only in component state, never a
TanStack mutation/cache, browser storage, URL or telemetry. Cancel, confirmation, expiry and
unmount discard the seed; pending metadata after a remount offers cancellation, never readback.
QR rendering uses the already installed qrcode@1.5.4 browser entry, declared explicitly by
the web package. web/modules/account/qrcode.d.ts describes only the consumed Promise overload
toDataURL(text, { errorCorrectionLevel: 'M', width, margin }),
returning Promise<string>. The package ships no declarations; this narrow surface was checked
against its upstream README and installed lib/browser.js/lib/renderer/canvas.js, without
a new type package or unchecked casts. The component renders a local PNG data URL with a four-module
quiet zone and the library's default opaque black/white colors independent of theme. Failure leaves the manual setup key available;
there is no external QR service, HTML injection or persisted image.
The browser consumes the shared AccountSecurityStatus, TotpEnrollment and
LoginSessionsPage DTOs through the existing client/query owner. Eligibility is the server's
password evidence, not a password-hash or external-identity guess. Enrollment asks for the current
password and confirms an unused six-digit string; disabling requires an unused code and ends the
current session as well as other browser sessions. The server owns revisions, replay prevention,
absolute expiry and all authorization. API tokens and paired computers are separate authorities.
LoginSessionsSection uses cursor pages and CardGrid layout="list"; a row is named by the session's own
device — Browser · OS, or the parser's mobile model — projected once by
packages/db/src/users/sessionDevice.ts from the User-Agent kept at sign-in, with a localized Unknown device
for rows that predate it. The row also shows reliable start/expiry dates and the authentication method, and the
current session carries its marker as the row's status. The name is self-reported identification, never an
authorization signal, and the raw header never reaches the browser.
Revoke uses ConfirmDialog and explains the account-wide browser reconnect. Session trust uses
the existing security.tokenTtlDays lifetime, not a device cookie or another setting.
LoginForm keeps the same card at the requested address, including CLI approval queries.
totp_required never invokes onAuthed; only final authentication opens the gate. The code
is text with numeric input mode, one-time-code autocomplete, leading-zero support and outer-only
clipboard trimming. Back cancels the pending BFF-owned challenge and clears the password/code.
Wrong-code errors remain local preauthentication errors, not session expiry.
The existing Users Account tab reads totpEnabled/securityRevision from its human directory
row. Reset targets another human account only, requires explicit confirmation and a closed
password-plus-optional-actor-TOTP or recent-Microsoft proof. Actor factor state comes from
accountSecurity, not the target or a stored hash. Success refreshes the directory; stale state
and uncertain writes never show a success toast. The daemon remains the final authority for
current admin role, actor/target separation and impersonation restrictions. No recovery-code,
CLI-recovery or hidden-reset path exists.
A security write can commit before live transport cleanup fails. The daemon then returns
503 { error: 'auth_security_cleanup_failed', committed: true }. The three UI consumers
distinguish that exact typed marker from an ordinary refusal: they warn that the change was
saved but closing sessions could not be verified, discard provisioning/actor proof state and
never repeat the OTP write automatically. Enrollment confirmation and disable end the local
session through clearToken; session revocation does so only when the selected session was
current, otherwise it refreshes the session list. An administrator resetting another account
remains signed in, closes the proof dialog and invalidates the directory and actor security
status. This outcome is neither a rollback nor a fully verified success.
Account > Computers
ElowenCliSection (web/modules/account/ElowenCliSection.tsx) is the browser half of the core CLI module, built
from the same parts as API tokens (SettingsGroup, SettingsRow, the shared states, ConfirmDialog, toasts). It
holds three groups, top to bottom — the approval card, the install row and the account's own computers, which
are Projects (executionKind 'local') rather than a second list of devices:
- The approval card, only while the address carries
code=(theverificationUriCompletethatelowen loginprints,/account?cat=elowenCli&code=<userCode>).useCliDeviceCodereadsGET /cli/device; Approve and Deny post throughuseDecideCliDeviceCode. The request carries itskind, and the card follows it: anaccountsign-in for the daemon on this host is titled Approve server CLI sign-in, says the terminal gets a personal API token and that no computer is paired, labels its first row Terminal and toasts the sign-in as approved; acomputerpairing keeps Approve sign-in, the Device row and the device-approved toast. A 410 (unknown or expired) and a 409 (already decided), from the lookup or the decision, are outcomes with a Close action, not errors. After a decision, and on Close,codeis removed withannounceLocation, so the section stays and a reload asks nothing again. The section readscodefromwindow.locationon arrival and onpopstate, like the deck readscat:announceLocationkeeps Next's history state, souseSearchParamsdoes not see the change. A signed-out visitor gets the login form at the same address and lands on the card after signing in. - Install: one
SettingsRowin a compactSettingsGroup, like the API tokens "call" row: the label and a one-line description, and a single Copy install command button (variant="default" size="sm"). Nothing is minted on render and the command is never shown. A click runsuseCreateCliEnrollment(POST /cli/enrollments, at most one live ticket per account) with the button disabled until it settles, then writes the command withcopyText(web/lib/clipboard.ts) and toasts Copied.useCliEnrollmentreads the server-owned lock throughGET /cli/enrollmentson mount and every 10 seconds. It samples the server'sremainingMsagainstperformance.now(), avoiding browser clock skew, and restores the disabled tabularformatClockcountdown after F5. Changing the installer keeps this ticket's lock. At expiry or after a status read reports redemption, the normal label and action return. Tickets and locks disappear on daemon restart. The accessible name gives the local availability time without announcing every second. A failed mint toastsinstallError; a concurrent mint's typed 409 refreshes the lock. A refused clipboard write cancels that exact mint throughDELETE /cli/enrollments/:idbefore showingcommon.copyFailed, so another attempt is available. Failed status reads show the shared retryable error and disable minting. The write follows an awaited fetch: Chromium and Firefox accept it, Safari may refuse it and then shows the copy-failed toast. The installer follows the browser's system, read once after mount bybrowserOs(Chromium'suserAgentData.platform, else the user agent string with phones, tablets, ChromeOS and iPadOS's Mac claim ruled out): Windows mintspowershelland its description names PowerShell, macOS and Linux mintshand name Terminal or a terminal, and no choice is shown. Only an unknown system (a phone) gets the choice, aSegmentedof two icon-only options (iconOnly): Windows, and macOS with Linux, which share the sh installer. Their names (installerWindows,installerUnix) are the radios' accessible names and tooltips, sh is preselected, and the description follows the pick. The marks areweb/public/os/{windows,apple,linux}.svg, single-path monochrome SVGs drawn throughAdaptiveBrandMark monochrome, so the CSS mask takes the skin's text colour; each file names its source (Microsoft's VS Code Windows mark, apple.com's navigation mark, and Simple Icons' CC0 Tux, which cites the Linux Foundation's Linux mark page). - Computers: the list is the Project register's own read.
useProjects()(GET /projects, re-read every 10 s while the list holds a computer, which is also how the live load stays current) is narrowed to the computers this account PAIRED —executionKind === 'local'with acomputerandcreatorUserIdequal to the reader's account id. A computer somebody shared with the account is worked in from the register, where its members manage it; it is not listed here. ONE My computers card holds them: while the list loads, fails or is empty it carries the loading, retryable error or empty state; otherwise one folding item per computer, the sharedComputerUsageItem(web/modules/computers/ComputerUsage.tsx), separated by hairlines and folded by default. That item is the SAME component theprojects/register's computer card and drawer draw their facts from, so the two surfaces cannot disagree about a state, a reading or a wording. The items are the host's collapsibleSettingsGroupin its unframed form, the one Skills, MCP and Agents fold their groups with (collapsiblewith the item's ownopenstate, nostorageKey), so the chevron, fold button and keyboard behaviour are the shared ones. The section keeps ONE range for every item, remembered underelowen.account.cliDevices.range(default 7 days, capped atAPI_TOKEN_STATS_MAX_DAYS) and read as UTC days throughutcDayWindow. It is shown only inside an unfolded item, as aDateRangeFilteron the chart's legend row (TimeSeriesChart's host-onlylegendEnd), and a change in one item applies to all; nothing sits in the card header while everything is folded. While an item's statistics load or fail there is no legend, so the filter stands alone, right-aligned, above that state. API tokens is built the same way (elowen.account.apiTokens.range; its header keeps only New token), from the same parts and no new chart or wrapper component:- Folded row: one line beside the name in the heading, at
text-metamuted with 12 px lucide icons: the 6 px status dot and Connected or Offline (DetailMeta), and last online (clock; only while offline,lastSeenAtor Not connected yet, since Connected already says now). Each fact keeps its label as screen-reader text; on a phone they wrap under the name. Beneath sits the muted line<OS> <arch> · client <version>(each part omitted while unknown), and on the right Disconnect (outline-danger). Between them, centred, the live load usesComputerLoadRingsand the shared compactResourceMeterstrack for CPU, memory and disk only, showing their shares and compact used capacity. IO and network stay in the expanded detail. Every reading opens the shared history window. It shows only while the item is folded (unfolded, the big rings close it), only with aloadreading, and only from the header's@2xlcontainer width, so a phone shows none. Disconnect opens the section'sConfirmDialogin the computer's own words and deletes its Project throughuseRemoveProject(DELETE /projects/:id); the daemon revokes the device token in the same act, and the register is refetched. Theprojects/drawer asks the register for the same removal instead of owning a second confirmation. - Unfolded:
ComputerUsageChartoverGET /cli/devices/:targetId/statsfor the section's range, enabled only while the item is open, so a folded computer costs no request.TimeSeriesChartdraws turns and commands as bars and failures and cost as lines, cost on the right axis (--color-chart-1..4), one zero-filled point per day of the window; an unpriced day stays unknown rather than zero. One totals line follows: turns, commands, files edited, tokens and cost, with an unknown cost said as unknown. - Load now: the computer's latest reading (
computer.load) as fiveResourceMetersrings for CPU (detail: cores), memory and disk (detail: used / total), IO and network (absolute speeds), centred at the foot of the unfolded item in the host-onlysize="large"(76 px rings) under a hairline and a centred caption (ComputerLoadRings). Without a reading (offline, or connected before its first sample) the rings keep their unknown state and a muted line says why; a CPU share not measured yet keeps the core count.
- Folded row: one line beside the name in the heading, at
The register's card and drawer are documented in "Computer pieces" in docs/modules/web/components.md.
The Playwright fake daemon serves these routes from web/tests/e2e/fake-daemon/handlers/cliDevices.ts: one
computer is one Project, so POST /__test/cli-devices pairs BOTH — it appends the Project to the projects
snapshot AND records the device its statistics are keyed on (each seed optionally with connected, platform,
arch, version, lastSeenAt, a compact load ({ cpu, cores, mem, disk }) and usage days given as
daysAgo offsets; a device without days answers an empty window). DELETE /projects/:id revokes the device,
as the daemon does. Faults are armed through the override keys: projects/read-state ('slow', 'error')
holds or fails the list, and cli/devices/stats ({ delay }, { status }) does the same for one computer's
chart. web/tests/e2e/specs/account.elowenCli.e2e.ts covers the flows.
What the account page says about a field
The Account page does not mark which fields are inherited and has no "revert to default" action: a self route answers with the resolved values and the revision only. An account's own value is cleared by an administrator through Apply to everyone, never from the Account page.
Every account save diffs against the last canonical answer for the same reason the window does. A client that sends its whole form on any edit stores inherited values as the account's own and silently ends their inheritance.
DTOs and hooks
web/lib/accountDefaults.ts states the field list, the wire shapes of /users/defaults,
/users/defaults/apply and /users/defaults/plugins. The self routes use the plain CliSettings,
PermissionSettings and MemoryDreamingSettingsPatch types. The authoritative field vocabulary and
stored-key mapping live in packages/db/src/config/accountDefaults.ts; the config repositories resolve
the inheritance chain. The browser mirrors the field list in web/lib/accountDefaults.ts without
importing the persistence implementation. useUsersDefaults and useUsersDefaultsPlugins read the two listings,
useSaveUsersDefaults and useApplyUsersDefaults write them, and useSaveMyCliSettings,
useSaveMyPermissions and useSaveMyMemoryDreaming take those plain patches.