Related user story
#39178
Task
Render the new notification activities in the global feed, the host feed, the details modals, and the policy Automation runs table. Each failure cause gets its own sentence, so an admin can tell "deploy Fleet Desktop" apart from "the screen was locked."
Depends on the patch notification kind, which defines the activity payload.
Activity feed
frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem.tsx handles the global feed. The patch-when-closed skip is already handled at line 1529, which is the pattern to copy.
Global feed:
Fleet notified end user 1 hour before patching **1Password**, **Slack**, and **Docker Desktop** on **John's MacBook Pro**.
Fleet failed to notify end user 1 hour before patching **1Password** on **Josh's MacBook Pro**.
- The reminder uses
5 minutes in place of 1 hour, driven by time_before (3600 or 300).
Host feed (frontend/pages/hosts/details/cards/Activity/ActivityItems/, alongside InstalledSoftwareActivityItem.tsx:34) drops the host name:
Fleet notified end user 1 hour before the **1Password** patch on this host.
Truncate past three apps. List the first three, then , and <n> more apps. Figma dev note 5546:46306. The details modal lists all of them.
Bold the software titles and the host name. Use getDisplayedSoftwareName for the label and the raw name for any icon, per the frontend conventions.
Details modals
The activity payload carries no reason field. Marko settled this on the parent story: the modal fetches the notification script's result and picks its copy from the exit code.
The activity carries script_execution_id (#50679, and Figma dev note 5582:46519). Fetch the run with GET /api/v1/fleet/scripts/results/{execution_id}, which returns exit_code and output. SoftwareUninstallDetailsModal.tsx:191 already does exactly this with a scriptExecutionId, so follow it.
| Exit code |
Text |
0 |
If the host is offline when the patch is forced, Fleet skips the patch. When the host comes back online Fleet notifies the end user again and the patch is forced 1 hour later. |
100 |
The Fleet Desktop app is required to notify end users. Add the app from the Fleet-maintained catalog and deploy to all your hosts. |
101 |
The Fleet Desktop app v1.5.0 is required to notify end users. Add the app from the Fleet-maintained catalog and deploy to all your hosts. |
30, 31 |
The notification couldn't load. Fleet will try again on the next policy run. |
41 |
The screen was locked so the end user couldn't see the notification. Fleet will try again on the next policy run. |
| the deferred code, number TBC |
Another notification was displayed. Fleet will try again on the next policy run. |
no script_execution_id |
same sentence, see below |
| anything else |
no sentence, output only |
Deferred arrives two ways, and both render the same sentence. Marko added a notify exit code for "another notification is displayed" on 2026-08-11, so the binary reports it when it catches the condition. When the dispatcher catches it first no script runs at all, so there is no execution and no exit code, and absence of script_execution_id is the only signal. Absence is unambiguous because the patch kind sets no expires_at, making a server-side deferral the only activity-emitting outcome with no script.
Get the exit code number from Marko before implementing.
Default to no sentence. Exit codes get added over time, and a missing case must render the output rather than crash or show a wrong cause.
The modal also shows an Apps row listing every title, and the script output under Notification script output:.
Skip details copy
The app-open skip reuses the existing installed_software activity for both patch_when_closed and notify_before_patching. The two are distinguished by pre_install_query_output, which #50677 documents as different text per flag:
| Flag |
Output |
patch_when_closed |
Query didn't return result\nThe app was open. |
notify_before_patching |
Query didn't return result\nThe app was open. Fleet notifies the end user 1 hour before the patch is forced. |
Render the extra copy from that output rather than adding a field. Marko confirmed this on the parent story.
Host activity details modals reuse the global ones. Figma dev note 5546:43145.
Automation runs
frontend/pages/policies/details/components/PolicyAutomationsActivitiesTable/. The helpers at helpers.tsx:25 and :64 already map the app-open skip to a distinct label and a grey icon. Add:
| Automation |
Icon |
Details |
End user notified (1Password) |
grey error-outline |
End user was notified. Patch will be forced in 1 hour. If the host is offline when a patch should be forced, Fleet notifies the end user again when it comes back online and patches it after 1 hour. |
Failed to notify (1Password) |
red error-outline |
the sentence for that exit code, from the table above |
The reminder row reads Patch will be forced in 5 minutes.
The existing Install skipped (1Password) row keeps its copy: The app was open. The end user will be notified before the patch is forced. Check whether it needs updating now that notification is a real outcome.
One notification, several rows. A notification covering three policies appears in the Automation runs table of all three, showing that policy's app name. The backend join handles this; the table renders whatever it returns.
Interfaces
Add notified_end_user_before_patching to ActivityType in frontend/interfaces/activity.ts, and its payload fields to IActivityDetails. One type, not two: status distinguishes displayed from failed.
Both names already exist, and they mean different things. The activity payload field is install_skipped_when_app_open (server/fleet/activities.go:1219). skipped_install is the frontend display status derived from it, at frontend/interfaces/software.ts:483 and ActivityDetails/InstallDetails/constants.ts:22,38. Reuse both as they are. The merged doc #49106 conflates them by documenting the payload field as skipped_install; that is a doc fix, tracked in #50918.
Condition of satisfaction
Global feed
- One app, three apps, and five apps each render correctly, with the
, and 2 more apps suffix past three.
time_before: 3600 renders 1 hour; 300 renders 5 minutes.
- A failure renders the
failed to notify sentence.
- Software titles and the host name are bold.
Host feed
- The same activities render without the host name, ending
on this host.
Details modals
- Each documented exit code renders its sentence. An undocumented exit code renders output only, and does not crash.
- An activity with no
script_execution_id renders the deferred sentence and makes no script-results request.
- An activity carrying the deferred exit code renders the same sentence.
- The skip details copy differs between a
patch_when_closed skip and a notify_before_patching skip, driven by pre_install_query_output.
- The Apps row lists every app, including past three.
- Notification script output is shown.
- Host and global modals render identically.
Automation runs
End user notified and Failed to notify rows appear with the right icon colour and details text.
- The reminder row says
5 minutes.
- A notification covering three policies appears in all three tables.
- Regression:
Install skipped and the existing rows are unchanged.
Tests
yarn test extends GlobalActivityItem.tests.tsx, InstalledSoftwareActivityItem.tests.tsx, SoftwareInstallDetailsModal.tests.tsx, and PolicyAutomationsActivitiesTable.tests.tsx.
npx tsc --noEmit -p tsconfig.json
make lint-js
Related user story
#39178
Task
Render the new notification activities in the global feed, the host feed, the details modals, and the policy Automation runs table. Each failure cause gets its own sentence, so an admin can tell "deploy Fleet Desktop" apart from "the screen was locked."
Depends on the patch notification kind, which defines the activity payload.
Activity feed
frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem.tsxhandles the global feed. The patch-when-closed skip is already handled at line 1529, which is the pattern to copy.Global feed:
Fleet notified end user 1 hour before patching **1Password**, **Slack**, and **Docker Desktop** on **John's MacBook Pro**.Fleet failed to notify end user 1 hour before patching **1Password** on **Josh's MacBook Pro**.5 minutesin place of1 hour, driven bytime_before(3600 or 300).Host feed (
frontend/pages/hosts/details/cards/Activity/ActivityItems/, alongsideInstalledSoftwareActivityItem.tsx:34) drops the host name:Fleet notified end user 1 hour before the **1Password** patch on this host.Truncate past three apps. List the first three, then
, and <n> more apps. Figma dev note5546:46306. The details modal lists all of them.Bold the software titles and the host name. Use
getDisplayedSoftwareNamefor the label and the rawnamefor any icon, per the frontend conventions.Details modals
The activity payload carries no
reasonfield. Marko settled this on the parent story: the modal fetches the notification script's result and picks its copy from the exit code.The activity carries
script_execution_id(#50679, and Figma dev note5582:46519). Fetch the run withGET /api/v1/fleet/scripts/results/{execution_id}, which returnsexit_codeandoutput.SoftwareUninstallDetailsModal.tsx:191already does exactly this with ascriptExecutionId, so follow it.0If the host is offline when the patch is forced, Fleet skips the patch. When the host comes back online Fleet notifies the end user again and the patch is forced 1 hour later.100The Fleet Desktop app is required to notify end users. Add the app from the Fleet-maintained catalog and deploy to all your hosts.101The Fleet Desktop app v1.5.0 is required to notify end users. Add the app from the Fleet-maintained catalog and deploy to all your hosts.30,31The notification couldn't load. Fleet will try again on the next policy run.41The screen was locked so the end user couldn't see the notification. Fleet will try again on the next policy run.Another notification was displayed. Fleet will try again on the next policy run.script_execution_idDeferred arrives two ways, and both render the same sentence. Marko added a
notifyexit code for "another notification is displayed" on 2026-08-11, so the binary reports it when it catches the condition. When the dispatcher catches it first no script runs at all, so there is no execution and no exit code, and absence ofscript_execution_idis the only signal. Absence is unambiguous because the patch kind sets noexpires_at, making a server-side deferral the only activity-emitting outcome with no script.Get the exit code number from Marko before implementing.
Default to no sentence. Exit codes get added over time, and a missing case must render the output rather than crash or show a wrong cause.
The modal also shows an Apps row listing every title, and the script output under
Notification script output:.Skip details copy
The app-open skip reuses the existing
installed_softwareactivity for bothpatch_when_closedandnotify_before_patching. The two are distinguished bypre_install_query_output, which #50677 documents as different text per flag:patch_when_closedQuery didn't return result\nThe app was open.notify_before_patchingQuery didn't return result\nThe app was open. Fleet notifies the end user 1 hour before the patch is forced.Render the extra copy from that output rather than adding a field. Marko confirmed this on the parent story.
Host activity details modals reuse the global ones. Figma dev note
5546:43145.Automation runs
frontend/pages/policies/details/components/PolicyAutomationsActivitiesTable/. The helpers athelpers.tsx:25and:64already map the app-open skip to a distinct label and a grey icon. Add:End user notified (1Password)error-outlineEnd user was notified. Patch will be forced in 1 hour. If the host is offline when a patch should be forced, Fleet notifies the end user again when it comes back online and patches it after 1 hour.Failed to notify (1Password)error-outlineThe reminder row reads
Patch will be forced in 5 minutes.The existing
Install skipped (1Password)row keeps its copy:The app was open. The end user will be notified before the patch is forced.Check whether it needs updating now that notification is a real outcome.One notification, several rows. A notification covering three policies appears in the Automation runs table of all three, showing that policy's app name. The backend join handles this; the table renders whatever it returns.
Interfaces
Add
notified_end_user_before_patchingtoActivityTypeinfrontend/interfaces/activity.ts, and its payload fields toIActivityDetails. One type, not two:statusdistinguishes displayed from failed.Both names already exist, and they mean different things. The activity payload field is
install_skipped_when_app_open(server/fleet/activities.go:1219).skipped_installis the frontend display status derived from it, atfrontend/interfaces/software.ts:483andActivityDetails/InstallDetails/constants.ts:22,38. Reuse both as they are. The merged doc #49106 conflates them by documenting the payload field asskipped_install; that is a doc fix, tracked in #50918.Condition of satisfaction
Global feed
, and 2 more appssuffix past three.time_before: 3600renders1 hour;300renders5 minutes.failed to notifysentence.Host feed
on this host.Details modals
script_execution_idrenders the deferred sentence and makes no script-results request.patch_when_closedskip and anotify_before_patchingskip, driven bypre_install_query_output.Automation runs
End user notifiedandFailed to notifyrows appear with the right icon colour and details text.5 minutes.Install skippedand the existing rows are unchanged.Tests
yarn testextendsGlobalActivityItem.tests.tsx,InstalledSoftwareActivityItem.tests.tsx,SoftwareInstallDetailsModal.tests.tsx, andPolicyAutomationsActivitiesTable.tests.tsx.npx tsc --noEmit -p tsconfig.jsonmake lint-js