Automations and webhooks for the AT Protocol airglow.run

fix(logs): word and show the filtered-out count only where it applies #65

Closed tyb.there-hugo.dev
Closed opened by tyb.there-hugo.dev

Problem #

The filtered-out alert and row (95cd138, #14) have several copy and display problems:

  • They always render, even for automations with no conditions.
  • The count prints unformatted (for example 4382112).
  • In dry run, and after partial runs, the copy says these events "do not appear below", which is false.
  • Refresh does not update the count.

Evidence #

  • app/routes/u/[handle]/[rkey]/logs.tsx:85-99, app/routes/u/[handle]/[rkey]/index.tsx:288-303.
  • Dry-run gate stops write a "Would stop" row and are also counted: lib/jetstream/run-steps.ts:390-403.
  • app/routes/api/automations/[rkey].ts returns no count.

Expected #

  • The alert and row show only when the automation has at least one condition step (or legacy conditions).
  • The count uses toLocaleString().
  • The copy no longer claims the events are absent from the log when dry-run or partial-run rows exist.
  • The Refresh button updates the count.

Acceptance criteria #

Out of scope #

Listing the rejected events (separate issue).

Queued: the loop will work on this version of the issue.

loop:queued cid=bafyreib2dxouxotg5smdzdo6sl6xfeibig5d4chvrcimhed3uti65v5pna

The issue changed after it was marked ready, so the loop will not read the new text. Mark it ready again to queue this version.

loop:awaiting-owner run=none why=edited after=2026-10-28T11:31:41.611Z

Queued: the loop will work on this version of the issue.

loop:queued cid=bafyreibarvtsx3dnc2z5exeah2wdg6jlqlwhqn4sghnvbebaudnzfwibde

Working on this, attempt 1.

loop:claim run=contribute-20261028-133400-3myumsnqbej22 branch=loop/3myumsnqbej22 attempt=1 issue=65

fix(logs): word and show the filtered-out count only where it applies

The filtered-out alert (logs page) and row (automation page) now show only for an automation with a condition that can filter an event out, print the count with toLocaleString(), and no longer claim the filtered events are absent from the log. GET /api/automations/:rkey now returns filterSummary, and the alert moved into the DeliveryLog island so its Refresh button updates the count along with the rows.

What changed #

  • lib/jetstream/filter-summary.ts (new, no database import, so the island can use it): FILTER_SUMMARY_DAYS and FilterSummary (moved from filter-counts.ts, which re-exports them), hasFilterGates, filteredEventsText ("1,234 events") and filteredNote(dryRun), the shared copy.
  • Copy. Live: "They are counted, not logged: the delivery log shows only the steps that ran before each stop, if any." Dry run: "They are counted. In dry run, most also have a row in the delivery log saying why they stopped; only those turned away by event-only conditions at the very start of the steps have none." Both pages use the same note, so it says "the delivery log" and not "below".
  • GET /api/automations/:rkey returns filterSummary (null when the automation has no gates). DeliveryLog takes filtered and dryRun props, renders the alert under its header, and Refresh sets both from the response.
  • logs.tsx no longer renders the alert itself; its layout is back to the single card it had before #14. index.tsx skips the row and its query when there are no gates.

Verification #

  • pnpm run check passes. pnpm run test passes: 177 files, 2733 tests.
  • New and updated tests:
    • filter-summary.test.ts: what counts as a gate, the count formatting, and the note wording.
    • logs.test.tsx: 1,234 renders as "1,234", the dry-run copy, and no alert without conditions.
    • index.test.tsx: no row without conditions or with only branches and loop gates, the row shown for a steps gate, 1,234 rendered as "1,234", and the dry-run copy.
    • [rkey].test.ts: the API returns the summary, or null without gates.
    • DeliveryLog.test.tsx: Refresh updates the count to "4,382,112", switches to the dry-run copy, removes the alert when the API sends null, and keeps focus on the Refresh button.
  • Keyboard: no new control. Refresh is still the same native button and keeps focus when the alert re-renders. This is checked in a happy-dom test only: I did not drive a browser, since that needs the local PDS for sign-in.

Acceptance criteria #

Decisions you can reverse #

  • What counts as "has gates". For the steps model this means a gate (a condition with no then) anywhere except inside a loop body, branch arms included. A branch never stops a run, and a gate inside a loop only skips an item, so neither is ever counted. Showing the alert for them would show "0 filtered" forever. The issue said "at least one condition step". Switching to that is a one-line change in hasFilterGates. For legacy rows, per-fetch conditions count as well as trigger conditions, because their skips are counted too (handler.ts).
  • Wording follows the automation's current dryRun. The 7-day count can mix live and dry-run days. The live note is true either way, and the dry-run note hedges with "most".
  • The alert now sits inside the log card, under the "Delivery Logs" header next to Refresh, instead of above the card. That is what lets Refresh update it.

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/3myumsnqbej22

loop:pushed run=contribute-20261028-133400-3myumsnqbej22 sha=0b74011829055790faead4271aeefefb11626812

Verdict: changes-requested. Most of the branch is right. The alert and row are gated by hasFilterGates, the count uses toLocaleString(), the live copy is accurate, and GET /api/automations/:rkey returns filterSummary, which DeliveryLog's Refresh now renders. One criterion is still not met: "The copy is accurate for dry-run automations". The new dry-run note says "most also have a row in the delivery log". That is false for the most common dry-run automation, one whose conditions are event-only gates at the start of the steps (the shape the form's hoist advice steers authors toward). For that shape, every filtered event is rejected by the consumer's trigger gate and none gets a row.

What I checked and how:

  • pnpm run check passes. pnpm run test passes: 177 files, 2733 tests.
  • I found every place a filtered event is counted (noteFiltered):
    • consumer.ts:893: the trigger gate, or legacy trigger conditions. The handler is never called, so no row is written in either mode.
    • run-steps.ts:402: an in-tree gate. In dry run it writes a "Would stop" row. A gate in a loop is not counted.
    • handler.ts:255: a legacy per-fetch condition. In dry run it writes a "Skipped" row.
  • hasFilterGates matches those sites. It searches branch arms and skips loop bodies, and for legacy rows it covers trigger and per-fetch conditions. The "has gates" reading the PR chose (only gates that can end a run) fits the issue's "no alert for an automation without gates" criterion.
  • I found every reader of the summary and its copy: index.tsx, logs.tsx, the API route and the DeliveryLog island. No other place renders the old "do not appear below" text. Refresh keeps focus on the button (covered by a happy-dom test), and the API already returns dryRun for setDryRun.
  • I tried the real input with a throwaway test in lib/jetstream/consumer.test.ts, since reverted. It used a dryRun: true steps automation with [gate on event.commit.record.subject.uri, notification] and 5 plain like events that fail the gate. The handler was never called, so no row was written, and noteFiltered was called 5 times. The alert for this automation would say "5 events ... In dry run, most also have a row in the delivery log", while the log shows none of them.

Changes:

  1. In lib/jetstream/filter-summary.ts, change filteredNote(true) so it makes no claim about how many filtered events have rows. Suggested wording: "They are counted. In dry run, a condition later in the steps also writes a row to the delivery log saying why it stopped the run; events turned away by event-only conditions at the very start have no row." Each part of that wording holds against the three counting sites above. An in-tree gate writes "Would stop" and a legacy fetch condition writes "Skipped". The trigger gate and legacy trigger conditions are the event-only checks at the start, and they write no row. You may go further and choose the note by the automation's shape, using the live note in dry run when every run-ending gate is in the trigger-gate prefix (triggerGatePrefixLength) or, for legacy rows, when there are no per-fetch conditions. If you do, the API must return what Refresh needs to pick the same note (for example the note text, or a boolean next to dryRun), so the server and the island agree.
  2. Update the tests that pin the old text ("In dry run, most also have a row in the delivery log") in app/islands/DeliveryLog.test.tsx, app/routes/u/[handle]/[rkey]/logs.test.tsx and app/routes/u/[handle]/[rkey]/index.test.tsx to the new text. In lib/jetstream/filter-summary.test.ts, add an assertion that the dry-run note does not claim "most" have a row.

loop:verdict run=review-20261028-134449-3myumsnqbej22 sha=0b74011829055790faead4271aeefefb11626812 verdict=changes-requested

Fix round 1: the review asked for changes.

loop:fix run=contribute-20261028-134843-3myumsnqbej22 round=1 issue=65

fix(logs): word and show the filtered-out count only where it applies

The filtered-out alert (logs page) and row (automation page) now show only for an automation with a condition that can filter an event out, print the count with toLocaleString(), and no longer claim the filtered events are absent from the log. GET /api/automations/:rkey now returns filterSummary, and the alert moved into the DeliveryLog island so its Refresh button updates the count along with the rows.

What changed #

  • lib/jetstream/filter-summary.ts (new, no database import, so the island can use it): FILTER_SUMMARY_DAYS and FilterSummary (moved from filter-counts.ts, which re-exports them), hasFilterGates, filteredEventsText ("1,234 events") and filteredNote(dryRun), the shared copy.
  • Copy. Live: "They are counted, not logged: the delivery log shows only the steps that ran before each stop, if any." Dry run: "They are counted. In dry run, a condition later in the steps also writes a row to the delivery log saying why it stopped the run; events turned away by event-only conditions at the very start have no row." It makes no claim about how many have a row: for an automation whose gates are all event-only conditions at the start, none do. Both pages use the same note, so it says "the delivery log" and not "below".
  • GET /api/automations/:rkey returns filterSummary (null when the automation has no gates). DeliveryLog takes filtered and dryRun props, renders the alert under its header, and Refresh sets both from the response.
  • logs.tsx no longer renders the alert itself; its layout is back to the single card it had before #14. index.tsx skips the row and its query when there are no gates.

This round #

The review found the dry-run note's "most also have a row in the delivery log" false for the common shape, an automation whose gates are all event-only at the start of the steps: the consumer's trigger gate rejects those events and writes no row. The note now uses the review's suggested wording, the three page and island tests pin the new text, and filter-summary.test.ts asserts the dry-run note does not say "most".

Verification #

  • pnpm run check passes. pnpm run test passes: 177 files, 2734 tests.
  • New and updated tests:
    • filter-summary.test.ts: what counts as a gate, the count formatting, and the note wording, including that the dry-run note makes no "most" claim.
    • logs.test.tsx: 1,234 renders as "1,234", the dry-run copy, and no alert without conditions.
    • index.test.tsx: no row without conditions or with only branches and loop gates, the row shown for a steps gate, 1,234 rendered as "1,234", and the dry-run copy.
    • [rkey].test.ts: the API returns the summary, or null without gates.
    • DeliveryLog.test.tsx: Refresh updates the count to "4,382,112", switches to the dry-run copy, removes the alert when the API sends null, and keeps focus on the Refresh button.
  • Keyboard: no new control. Refresh is still the same native button and keeps focus when the alert re-renders. This is checked in a happy-dom test only: I did not drive a browser, since that needs the local PDS for sign-in.

Acceptance criteria #

Decisions you can reverse #

  • What counts as "has gates". For the steps model this means a gate (a condition with no then) anywhere except inside a loop body, branch arms included. A branch never stops a run, and a gate inside a loop only skips an item, so neither is ever counted. Showing the alert for them would show "0 filtered" forever. The issue said "at least one condition step". Switching to that is a one-line change in hasFilterGates. For legacy rows, per-fetch conditions count as well as trigger conditions, because their skips are counted too (handler.ts).
  • Wording follows the automation's current dryRun. The 7-day count can mix live and dry-run days. The live note is true either way. The dry-run note says which conditions write a row and which do not, without saying how many filtered events have one. Choosing the note by the automation's shape (the live note in dry run when every gate is in the trigger-gate prefix) is possible but would need the API to send the choice too; I kept one fixed note per mode.
  • The alert now sits inside the log card, under the "Delivery Logs" header next to Refresh, instead of above the card. That is what lets Refresh update it.

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/3myumsnqbej22

loop:pushed run=contribute-20261028-134843-3myumsnqbej22 sha=21f915a959a87930ff49787f77ac971bbbafdcac

Verdict: merge. This round fixes the one gap the last review found. The dry-run note no longer says "most" filtered events have a row. It now uses the wording that review prescribed, which holds at all three counting sites: the trigger gate and legacy trigger conditions write no row, an in-tree gate writes "Would stop", and a legacy fetch condition writes "Skipped". The rest of the branch meets every acceptance criterion and is unchanged since the last review checked it: the gating, the toLocaleString() count, filterSummary in the API, and Refresh in DeliveryLog.

What I checked and how:

  • I read the delta from 0b74011 to 21f915a. It changes only the filteredNote(true) text and its doc comment in lib/jetstream/filter-summary.ts, the three tests that pinned the old text (DeliveryLog.test.tsx, logs.test.tsx, index.test.tsx), and a new assertion in filter-summary.test.ts that the note has no "most". Both changes the last review asked for are done.
  • I re-read the full diff against main, and nothing from the earlier round has regressed:
    • hasFilterGates still gates the row in index.tsx, the alert in logs.tsx and the API's filterSummary.
    • Refresh still sets both filtered and dryRun from GET /api/automations/:rkey.
    • logs.tsx is the only place that renders DeliveryLog.
    • No other place renders the old "do not appear below" or "counted, not logged" copy.
  • pnpm run check passes. pnpm run test passes: 177 files, 2734 tests.
  • vp build --mode client succeeds, so the island can import filter-summary.ts and steps.ts without pulling in the database.

One thing that does not block the merge: the prop comment on dryRun at app/islands/DeliveryLog.tsx:58 still says "dry run logs most filtered events", which is the claim this round removed from the copy. It is only a code comment, so it changes nothing users see, but it is worth correcting in a later change.

loop:verdict run=review-20261028-135322-3myumsnqbej22 sha=21f915a959a87930ff49787f77ac971bbbafdcac verdict=merge

Merged onto main as 3463e5e0ec11972598c105ca2539c606aab61e2c.

loop:merged sha=3463e5e0ec11972598c105ca2539c606aab61e2c head=21f915a959a87930ff49787f77ac971bbbafdcac

Sign up or Sign in to add to the discussion.