The Installed tab could only update one plugin at a time — one dialog and one watched job each. This adds the bulk action beside the list it acts on (the same place Sources keeps "Refresh all"), plus a count badge on the Installed tab trigger, because Browse is the tab the page opens on and a control nobody passes is a control nobody finds.
The run is a queue, not a fan-out
The host takes one package operation at a time (409 otherwise — bun operations share a lockfile and a node_modules tree), so the console works through the updates job by job, each install starting when the previous one settles rather than on a timer. The progress card gains an "Update 2 of 5" line, with a bridging card for the gap between packages so the run never appears to stall.
The run carries its own copy of what is left. Every finished install invalidates the installed list, and a queue that re-derived itself would change shape underneath a run the operator already confirmed.
Trust rules unchanged, taken once instead of N times
One dialog, listing every version change rather than a count — a bulk confirm that only says "3 plugins" is asking the operator to trust a number. If any entry in the run comes from an operator-added source, the whole dialog wears the external treatment and names those catalogs: a bulk action must not be a way to wave through, in one click, a warning each package would have shown on its own.
Updates the host would refuse never enter the queue — no catalog entry, incompatible (a 400 from POST /store/install), or a blocked entry — and the dialog names them, so the button's count still adds up on screen. plugin.blocked is deliberately not a reason to skip: that advisory is against the version installed right now, and updating away from it is the fix.
A failure ends the run. The failed job's card — phase, error, log — is the only record of what went wrong, and starting the next install would replace it with a fresh spinner; the toast says what was applied and what was not, and the rows are still there to retry from.
One adjacent bug, fixed because this leans on it
Disabled buttons were invisible, console-wide.AnimatedButton is a motion element and its mount animation settles as an inline opacity: 1, which outranks the disabled:opacity-50 class @unom/ui also ships — measured opacity: 1 on a disabled button. Only pointer-events: none was landing (nothing sets that inline), so every disabled control in the app looked live and silently swallowed the click. Corrected in the components/ui wrapper layer like the other @unom/ui adaptations; this also repairs the pre-existing per-row busy state.
Verification
tsc --noEmit, biome, bun test server/ (9/9), production build, i18n check — 650 messages, en + de
New Storybook stories for the list header (Store/InstalledList) and the confirm dialog (Store/UpdateAllDialog), rendered headless in light and dark, with the disabled states asserted on the DOM rather than judged by eye
Biome also reformatted two unrelated files (os-icon.tsx, displays.tsx) — pre-existing drift on main, reverted to keep the diff clean
Not verified: a real multi-package run against a live host. The sequencing is verified by construction and at component level, not on glass — worth one pass on a test box with two updatable plugins.
The Installed tab could only update one plugin at a time — one dialog and one watched job each. This adds the bulk action beside the list it acts on (the same place Sources keeps "Refresh all"), plus a count badge on the Installed tab trigger, because Browse is the tab the page opens on and a control nobody passes is a control nobody finds.
## The run is a queue, not a fan-out
The host takes **one package operation at a time** (`409` otherwise — `bun` operations share a lockfile and a `node_modules` tree), so the console works through the updates job by job, each install starting when the previous one settles rather than on a timer. The progress card gains an "Update 2 of 5" line, with a bridging card for the gap between packages so the run never appears to stall.
The run carries its own copy of what is left. Every finished install invalidates the installed list, and a queue that re-derived itself would change shape underneath a run the operator already confirmed.
## Trust rules unchanged, taken once instead of N times
One dialog, listing every version change rather than a count — a bulk confirm that only says "3 plugins" is asking the operator to trust a number. If **any** entry in the run comes from an operator-added source, the whole dialog wears the external treatment and names those catalogs: a bulk action must not be a way to wave through, in one click, a warning each package would have shown on its own.
Updates the host would refuse never enter the queue — no catalog entry, incompatible (a `400` from `POST /store/install`), or a blocked entry — and the dialog names them, so the button's count still adds up on screen. `plugin.blocked` is deliberately *not* a reason to skip: that advisory is against the version installed right now, and updating away from it is the fix.
A failure ends the run. The failed job's card — phase, error, log — is the only record of what went wrong, and starting the next install would replace it with a fresh spinner; the toast says what was applied and what was not, and the rows are still there to retry from.
## One adjacent bug, fixed because this leans on it
**Disabled buttons were invisible, console-wide.** `AnimatedButton` is a motion element and its mount animation settles as an inline `opacity: 1`, which outranks the `disabled:opacity-50` class `@unom/ui` also ships — measured `opacity: 1` on a `disabled` button. Only `pointer-events: none` was landing (nothing sets that inline), so every disabled control in the app looked live and silently swallowed the click. Corrected in the `components/ui` wrapper layer like the other `@unom/ui` adaptations; this also repairs the pre-existing per-row busy state.
## Verification
- `tsc --noEmit`, biome, `bun test server/` (9/9), production build, i18n check — 650 messages, `en` + `de`
- New Storybook stories for the list header (`Store/InstalledList`) and the confirm dialog (`Store/UpdateAllDialog`), rendered headless in light and dark, with the disabled states asserted on the DOM rather than judged by eye
- Biome also reformatted two unrelated files (`os-icon.tsx`, `displays.tsx`) — pre-existing drift on `main`, reverted to keep the diff clean
**Not verified:** a real multi-package run against a live host. The sequencing is verified by construction and at component level, not on glass — worth one pass on a test box with two updatable plugins.
The Installed tab could only update one plugin at a time, one dialog and one
watched job each. This adds the bulk action beside the list it acts on — the
same place Sources keeps "Refresh all" — plus a count badge on the Installed
tab trigger, because Browse is the tab the page opens on and a control nobody
passes is a control nobody finds.
The host takes ONE package operation at a time (409 otherwise: bun operations
share a lockfile and a node_modules tree), so this is a queue the console works
through job by job, driven by each job settling rather than by a timer. The run
carries its own copy of what is left: every finished install invalidates the
installed list, and a queue that re-derived itself would change shape underneath
a run the operator already confirmed.
Trust rules are unchanged, only taken once instead of N times. If any entry in
the run comes from an operator-added source the whole dialog wears the external
treatment and names those catalogs — a bulk action must not be a way to wave
through, in one click, a warning each package would have shown on its own. The
dialog lists every version change rather than a count, and names what it will
not attempt: an update with no catalog entry, or one this host would refuse
(400 on incompatible, blocked entries) never enters the queue, so the button's
count still adds up on screen.
A failure ends the run. The failed job's card is the only record of what went
wrong, and starting the next install would replace it with a fresh spinner; the
toast says what was applied and what was not, and the rows are still there to
retry from.
Also fixed, because this change leans on it: disabled buttons were invisible.
AnimatedButton is a motion element and its mount animation settles as an inline
`opacity: 1`, which outranks the `disabled:opacity-50` class the library also
ships — measured `opacity: 1` on a disabled button, console-wide. Only
`pointer-events: none` landed, so every disabled control in the app looked live
and silently ignored the click. Corrected in the components/ui wrapper layer
like the other @unom/ui adaptations.
Verified: tsc, biome, `bun test server/`, production build, i18n check (650
messages, en + de). Storybook stories added for the list header and the confirm
dialog; both rendered headless in light and dark, with the disabled states
asserted on the DOM rather than by eye.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The Installed tab could only update one plugin at a time — one dialog and one watched job each. This adds the bulk action beside the list it acts on (the same place Sources keeps "Refresh all"), plus a count badge on the Installed tab trigger, because Browse is the tab the page opens on and a control nobody passes is a control nobody finds.
The run is a queue, not a fan-out
The host takes one package operation at a time (
409otherwise —bunoperations share a lockfile and anode_modulestree), so the console works through the updates job by job, each install starting when the previous one settles rather than on a timer. The progress card gains an "Update 2 of 5" line, with a bridging card for the gap between packages so the run never appears to stall.The run carries its own copy of what is left. Every finished install invalidates the installed list, and a queue that re-derived itself would change shape underneath a run the operator already confirmed.
Trust rules unchanged, taken once instead of N times
One dialog, listing every version change rather than a count — a bulk confirm that only says "3 plugins" is asking the operator to trust a number. If any entry in the run comes from an operator-added source, the whole dialog wears the external treatment and names those catalogs: a bulk action must not be a way to wave through, in one click, a warning each package would have shown on its own.
Updates the host would refuse never enter the queue — no catalog entry, incompatible (a
400fromPOST /store/install), or a blocked entry — and the dialog names them, so the button's count still adds up on screen.plugin.blockedis deliberately not a reason to skip: that advisory is against the version installed right now, and updating away from it is the fix.A failure ends the run. The failed job's card — phase, error, log — is the only record of what went wrong, and starting the next install would replace it with a fresh spinner; the toast says what was applied and what was not, and the rows are still there to retry from.
One adjacent bug, fixed because this leans on it
Disabled buttons were invisible, console-wide.
AnimatedButtonis a motion element and its mount animation settles as an inlineopacity: 1, which outranks thedisabled:opacity-50class@unom/uialso ships — measuredopacity: 1on adisabledbutton. Onlypointer-events: nonewas landing (nothing sets that inline), so every disabled control in the app looked live and silently swallowed the click. Corrected in thecomponents/uiwrapper layer like the other@unom/uiadaptations; this also repairs the pre-existing per-row busy state.Verification
tsc --noEmit, biome,bun test server/(9/9), production build, i18n check — 650 messages,en+deStore/InstalledList) and the confirm dialog (Store/UpdateAllDialog), rendered headless in light and dark, with the disabled states asserted on the DOM rather than judged by eyeos-icon.tsx,displays.tsx) — pre-existing drift onmain, reverted to keep the diff cleanNot verified: a real multi-package run against a live host. The sequencing is verified by construction and at component level, not on glass — worth one pass on a test box with two updatable plugins.