Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add deployment event audit UI

Show event activity timeline on deployment detail page and trigger
reason column on deployment index. Events display human-readable
descriptions, timestamps, and actor SIDs.

sow-155

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+63 -6
+1 -1
apps/sower/lib/sower/orchestration/deployment.ex
··· 99 99 100 100 case Flop.validate_and_run(query, params, for: __MODULE__) do 101 101 {:ok, {deployments, meta}} -> 102 - {:ok, {Repo.preload(deployments, [:garden]), meta}} 102 + {:ok, {Repo.preload(deployments, [:garden, :events]), meta}} 103 103 104 104 {:error, meta} -> 105 105 {:error, meta}
+14 -2
apps/sower/lib/sower_web/live/deployment_live/index.ex
··· 29 29 30 30 @impl Phoenix.LiveView 31 31 def handle_info({:deployment, :created, deployment}, socket) do 32 - deployment = Sower.Repo.preload(deployment, [:garden]) 32 + deployment = Sower.Repo.preload(deployment, [:garden, :events]) 33 33 deployments = [deployment | socket.assigns.deployments] 34 34 {:noreply, assign(socket, :deployments, deployments)} 35 35 end 36 36 37 37 def handle_info({:deployment, :updated, deployment}, socket) do 38 - deployment = Sower.Repo.preload(deployment, [:garden]) 38 + deployment = Sower.Repo.preload(deployment, [:garden, :events]) 39 39 40 40 deployments = 41 41 Enum.map(socket.assigns.deployments, fn d -> ··· 122 122 end 123 123 124 124 defp filter_value(_meta, _field), do: nil 125 + 126 + defp trigger_label(deployment) do 127 + created_event = Enum.find(deployment.events, &(&1.event == :created)) 128 + 129 + case get_in(created_event.reason) do 130 + :user_triggered -> "user" 131 + :schedule_triggered -> "schedule" 132 + :realtime_triggered -> "realtime" 133 + :retry -> "retry" 134 + _ -> "-" 135 + end 136 + end 125 137 126 138 defp state_options do 127 139 ["created", "dispatched", "acknowledged", "completed", "stale", "canceled"]
+3
apps/sower/lib/sower_web/live/deployment_live/index.html.heex
··· 67 67 <:col :let={deployment} label="garden" field={:garden_name}> 68 68 {get_in(deployment.garden.name) || "-"} 69 69 </:col> 70 + <:col :let={deployment} label="trigger" hide_on={:sm}> 71 + {trigger_label(deployment)} 72 + </:col> 70 73 <:col :let={deployment} label="completed" field={:deployed_at} hide_on={:sm}> 71 74 <.local_datetime datetime={deployment.deployed_at} user_timezone={@user_timezone} /> 72 75 </:col>
+44 -2
apps/sower/lib/sower_web/live/deployment_live/show.ex
··· 126 126 No seeds. 127 127 </p> 128 128 </section> 129 + 130 + <section :if={@deployment.events != []}> 131 + <h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-200 mb-4">Activity</h2> 132 + <div class="space-y-3 text-sm"> 133 + <div 134 + :for={event <- Enum.sort_by(@deployment.events, & &1.inserted_at, DateTime)} 135 + class="grid grid-cols-[auto_auto_auto] gap-x-4 items-center justify-start" 136 + > 137 + <span class="text-zinc-400 dark:text-zinc-500"> 138 + <.local_datetime datetime={event.inserted_at} user_timezone={@user_timezone} /> 139 + </span> 140 + <span class="text-zinc-700 dark:text-zinc-300"> 141 + {event_description(event)} 142 + </span> 143 + <span class="text-zinc-400 dark:text-zinc-500"> 144 + {event.actor_sid} 145 + </span> 146 + </div> 147 + </div> 148 + </section> 129 149 </div> 130 150 </Layouts.app> 131 151 """ ··· 155 175 156 176 deployment -> 157 177 deployment = 158 - Sower.Repo.preload(deployment, seed_deployments: :seed, subscriptions: [], garden: []) 178 + Sower.Repo.preload(deployment, [ 179 + :events, 180 + seed_deployments: :seed, 181 + subscriptions: [], 182 + garden: [] 183 + ]) 159 184 160 185 {:noreply, 161 186 socket ··· 232 257 |> assign(:retrying, false) 233 258 end 234 259 260 + defp event_description(%{event: :created, reason: :user_triggered}), do: "Deployed by user" 261 + 262 + defp event_description(%{event: :created, reason: :schedule_triggered}), 263 + do: "Deployed by schedule" 264 + 265 + defp event_description(%{event: :created, reason: :realtime_triggered}), 266 + do: "Deployed by realtime trigger" 267 + 268 + defp event_description(%{event: :created, reason: :retry}), do: "Retried" 269 + defp event_description(%{event: :canceled, reason: :superseded}), do: "Canceled — superseded" 270 + defp event_description(%{event: :canceled, reason: :stale}), do: "Canceled — stale" 271 + 235 272 defp refresh_deployment(socket, sid) do 236 273 case Orchestration.get_deployment_sid(sid) do 237 274 nil -> ··· 239 276 240 277 deployment -> 241 278 deployment = 242 - Sower.Repo.preload(deployment, seed_deployments: :seed, subscriptions: [], garden: []) 279 + Sower.Repo.preload(deployment, [ 280 + :events, 281 + seed_deployments: :seed, 282 + subscriptions: [], 283 + garden: [] 284 + ]) 243 285 244 286 assign(socket, :deployment, deployment) 245 287 end
+1 -1
apps/sower/lib/sower_web/live/subscription_live/show.html.heex
··· 112 112 class="inline-flex items-center gap-1.5 align-middle hover:text-orange-500 dark:hover:text-orange-400" 113 113 > 114 114 <.result result={seed.latest_deployment_result} /> 115 - <span class="font-mono text-xs">{seed.latest_deployment_sid}</span> 115 + <span class="text-xs">{seed.latest_deployment_sid}</span> 116 116 </.link> 117 117 </:col> 118 118 <:col :let={seed} label="Updated" field={:updated_at} hide_on={:md}>
screenshot_2026-04-13-222454.png

This is a binary file and will not be displayed.

screenshot_2026-04-13-222510.png

This is a binary file and will not be displayed.