Mark a feed on the queue page when it has stopped importing

6029acf · Gabriel Morell · 2026-09-01 21:20

12 files +93 -0
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_source_live/work.ex
+23 −0
@@ -244,6 +244,29 @@ defmodule RoomSanctumWeb.SourceLive.Work do
244 244 end
245 245 end
246 246
247 + @doc """
248 + How long a feed has gone without importing, when that has become worth
249 + saying.
250 +
251 + Returns a bare number of days, or "N" for a feed that has never run, or nil
252 + while it is recent enough not to be interesting. Silent below the threshold
253 + on purpose: a badge on every feed all the time is a badge nobody reads, and
254 + the thing worth noticing is the one feed that has quietly stopped.
255 +
256 + A stale feed is not obviously broken from anywhere else in the app -- the
257 + board keeps showing yesterday's timetable, which looks like a timetable.
258 + """
259 + @stale_after_days 3
260 +
261 + def days_since_run(%{meta: %{last_run: %DateTime{} = at}}) do
262 + case DateTime.diff(DateTime.utc_now(), at, :day) do
263 + days when days > @stale_after_days -> days
264 + _recent -> nil
265 + end
266 + end
267 +
268 + def days_since_run(_source), do: "N"
269 +
247 270 @doc false
248 271 def source_id(%{args: %{"source_id" => id}}) when is_integer(id), do: id
249 272 def source_id(%{args: %{"source_id" => id}}) when is_binary(id), do: String.to_integer(id)
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_source_live/work.html.heex
+17 −0
@@ -76,6 +76,23 @@
76 76 <%!-- Live progress for whichever feed is loading, from the same
77 77 broadcasts the source page draws its bar from. Oban knows a
78 78 job is executing; only this knows it is four files in. --%>
79 + <%!-- Days since this feed last imported, and only once that has
80 + become worth saying. A feed that has stopped importing looks
81 + fine from everywhere else in the app: the board goes on
82 + showing yesterday's timetable, which looks like a timetable. --%>
83 + <% stale = days_since_run(source) %>
84 + <span
85 + :if={stale}
86 + class="badge badge-warning badge-sm"
87 + title={
88 + if stale == "N",
89 + do: "Never imported",
90 + else: "Last imported #{stale} days ago"
91 + }
92 + >
93 + <%= stale %>
94 + </span>
95 +
79 96 <%!-- The bar lives in the card above; here a feed only has to say
80 97 whether it is the one running. --%>
81 98 <%= if running && running.source_id == source.id do %>
modified apps/room_sanctum/test/room_sanctum_web/live/source_work_test.exs
+53 −0
@@ -188,6 +188,59 @@ defmodule RoomSanctumWeb.SourceWorkTest do
188 188 end
189 189 end
190 190
191 + describe "days since last run" do
192 + alias RoomSanctumWeb.SourceLive.Work
193 +
194 + # Written through update_source rather than update_source_meta: the latter
195 + # merges onto the existing embed, and a source created without one has meta
196 + # nil rather than empty.
197 + defp ran_days_ago(source, days) do
198 + at = DateTime.utc_now() |> DateTime.add(-days * 86_400) |> DateTime.truncate(:second)
199 + {:ok, source} = Configuration.update_source(source, %{meta: %{last_run: at}})
200 + source
201 + end
202 +
203 + test "a feed that has never imported says N", %{source: source} do
204 + assert Work.days_since_run(source) == "N"
205 + end
206 +
207 + test "a recent import says nothing at all", %{source: source} do
208 + # A badge on every feed all the time is a badge nobody reads.
209 + assert Work.days_since_run(ran_days_ago(source, 1)) == nil
210 + assert Work.days_since_run(ran_days_ago(source, 3)) == nil
211 + end
212 +
213 + test "past the threshold it says how many days", %{source: source} do
214 + assert Work.days_since_run(ran_days_ago(source, 4)) == 4
215 + assert Work.days_since_run(ran_days_ago(source, 30)) == 30
216 + end
217 +
218 + test "the chip is on the page, with the reason on hover", %{conn: conn, source: source} do
219 + ran_days_ago(source, 7)
220 +
221 + {:ok, _live, html} = live(conn, "/cfg/offerings/work")
222 +
223 + assert html =~ "Last imported 7 days ago"
224 + assert html =~ ~r/>\s*7\s*</
225 + end
226 +
227 + test "a never-run feed reads as never rather than as zero days", %{conn: conn} do
228 + {:ok, _live, html} = live(conn, "/cfg/offerings/work")
229 +
230 + assert html =~ "Never imported"
231 + assert html =~ ~r/>\s*N\s*</
232 + end
233 +
234 + test "a feed imported today carries no chip", %{conn: conn, source: source} do
235 + ran_days_ago(source, 0)
236 +
237 + {:ok, _live, html} = live(conn, "/cfg/offerings/work")
238 +
239 + refute html =~ "Never imported"
240 + refute html =~ "Last imported"
241 + end
242 + end
243 +
191 244 describe "clearing a stuck job" do
192 245 alias RoomSanctumWeb.SourceLive.Work
193 246

Parents: c59c3a0