- Status: done
- Date: 2026-05-30
- Specs touched: docs/specs/DASHBOARD.md (# install authorization, # warn-don't-block), docs/specs/BRAIN_UI_PROTOCOL.md (Pattern B — Jobs), docs/specs/APP_MANIFEST.md (# folders), docs/specs/APP_ISOLATION.md (# User content)
The final slice of the install consent flow: the Vue 3 frontend now fetches an InstallPlan, presents a consent + configuration dialog, and submits the user's elections to POST /api/v1/apps with config.folders[].
Added two types alongside the existing InstallPlan* interfaces:
FolderElection { folder, source?, subfolder? }— one entry in the install request'sconfig.foldersarray.InstallRequest { manifest_id, scope?, confirm?, config: { folders: FolderElection[] } }— the fullPOST /api/v1/appsbody for Door-1 catalog installs.
A modal dialog component. Props: plan: InstallPlan, submitError?: string | null. Emits: submit(InstallRequest), cancel. (Scope-picker visibility keys off scope_options.length, not the caller's role — the role is already baked into the plan, so the dialog needs no role prop.)
Features:
- Scope picker — shown only when
scope_options.length > 1(i.e. the caller is an admin). Radio buttons "For the whole household" / "Just for me"; default isscope_default. Members see a fixed "Installing as a personal app." label instead. - Permissions display (advisory, UI-authored sentences): internet → "Connect to the internet"; lan → "Reach other devices on your network"; gpu → "Use the graphics card"; each device path → "Access device ";
mode: readfolder → "Read files in your folder";mode: writefolder → "Add, change, and delete files in your folder" — write rows are styled intext-destructivewith bold weight (visually distinct per APP_MANIFEST.md:218 and APP_ISOLATION.md # User content). - Per-folder source pickers — reactive to the elected scope: reads
f.sources[electedScope](SourceMenu). A single-option menu (e.g. household forces "shared") renders as a fixed/disabled text label. A multi-option menu renders radio buttons with human labels: "Your " (personal) / "The household's shared " (shared). Source defaults are re-derived when the scope radio changes; subfolder user input is preserved across scope flips. - Subfolder input — only when
f.scope === "pick-subfolder": a text input pre-filled withsubfolder_default, labelled "Which subfolder should this app manage?". - 422 inline error — when
submitErroris set, displayed as a styled error block inside the dialog, keeping it open for correction. - Footer — Cancel and Install buttons. On Install, emits an
InstallRequestassembled from elected scope, per-folder sources, and per-folder subfolders.
All wording is UI-owned; no raw enum values are shown to the user.
- Imports
useAuth()to getcurrentUser(needed forowner_user_idcomparison and role). - Per-row button logic (replaces the old
installedManifest()hiding hack):- Household instance exists → "Open shared app" link to
instance.url+ a secondary "Install" button (allows installing a personal copy alongside the shared one). - Caller's own personal instance exists (
scope === "personal" && owner_user_id === currentUser.id) → "Open" link only. - Otherwise → "Install" button.
- Household instance exists → "Open shared app" link to
- Install flow: clicking Install sets
planForref →useQuery(["install-plan", id], enabled: planFor !== null)fetches theInstallPlan→ once loaded,<InstallDialog>renders. Dialog emitssubmit(InstallRequest)→installmutation posts to/apps. As soon as thePOSTis accepted (202 +job_id), the dialog closes and the catalog row's button switches to a disabled "Installing…" (driven by aninstallingIdref, not the dialog state).waitForJobthen polls the job to a terminal state;onSettledawaits the["apps"]invalidation before clearinginstallingId, so the row flips straight from "Installing…" to "Open" with no intermediate "Install" flicker. Because 409/422 are returned atPOSTtime (before the job starts), they still surface while the dialog is open; a failure during the job (after the dialog closed) shows in a standalone dismissableinstallErrorbanner. - 409 duplicate-install: on
ApiError.code === "duplicate-install",duplicateInfois set witherror.message; dialog hides and a warning banner appears with the summary and an "Install my own copy" button that re-submits the sameInstallRequestwithconfirm: true. - All other failures (422 election rejection, job failure, host 5xx):
dialogErroris set with the error message and passed as thesubmitErrorprop into the still-open dialog, displayed inline. The installmutationFnthrows whenwaitForJobreturns a non-completedterminal job (carryingjob.error.message), so an install that fails after the job starts is surfaced rather than silently closing the dialog. onSettledinvalidates["apps"]in both success and error paths.- The old
installedManifest()function and single-button hiding are removed. The header comment block is updated to describe the new flow.
Realizes the consent screen described in DASHBOARD.md # install authorization and # warn-don't-block. The scope picker follows the install authorization table (admin: household/personal choice; member: personal only). Per-folder source pickers follow APP_MANIFEST.md # folders ("source is the installer's choice") and the Option A source menus from the install-plan endpoint (slice 3). Write-mode warning styling matches the APP_MANIFEST.md:218 requirement ("shows up on the install screen as 'this app can ADD, CHANGE, AND DELETE files'"). The 409 duplicate-install pattern matches BRAIN_UI_PROTOCOL.md Pattern B and DASHBOARD.md # warn-don't-block exactly. The install mutation follows Pattern B (POST → 202 + job_id → poll to terminal state).
make check-webpasses (vue-tsc --noEmit + vite build, no TypeScript errors, build output 14.66 kB for StoreView chunk).- Runtime verification (install files-demo from the Store, admin scope picker, member forced-personal path, duplicate-install flow, 422 inline error display) is pending human verification — the implementation was not run against a live brain.
- Plan loading state UX: while the install plan is fetching (after clicking Install, before the dialog appears), a brief "Loading install plan…" text is shown below the catalog list rather than in-place on the row. A per-row spinner is a follow-up UX polish item.
- GPU/devices capacity errors deferred from slice 4 (see
install-permissions-enforcement.mdKnown gaps) — not a UI gap, but when the brain gains capacity-check enforcement those 422s will surface correctly via the existingdialogErrorpath.
- Mute settings UI + retention — the top item in the progress queue (
docs/progress/README.md# Up next).