Automations and webhooks for the AT Protocol airglow.run

fix(automations): keep edit-page autosave drafts when the server only changes the automation's active flag #63

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

Problem #

The edit-page autosave key hashes the whole server seed, including active. The form neither shows nor submits active, but the server flips it on dry-run expiry, rate limiting, session loss and redundant-automation sync, and so does the pause toggle. After any of these, the draft silently stops restoring, and the user's unsaved edits are lost with no notice.

Evidence #

  • app/routes/u/[handle]/[rkey]/edit.tsx:71-85 passes active: auto.active in initial.
  • app/islands/AutomationForm.tsx: autosaveContext includes initial, and the form has no other use of active.
  • Writers of active: false: lib/runtime/dry-run-expiration.ts:27, lib/jetstream/rate-limit.ts:177, lib/jetstream/session-loss.ts:39, lib/sync/redundant-automations.ts:103,115.
  • Commit 5e1491f (#33).

Expected #

A draft survives server changes to fields the form does not edit. When the seed did change in a field the form edits, the user is told a draft exists, not left with nothing.

Acceptance criteria #

Out of scope #

The blank-form key (separate issue), the scope-upgrade stash's context, and cross-tab sync.

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

loop:queued cid=bafyreigfleikinnhdymmgmnyqle5hq3cpex27dz3ruobxeiegaucumzlg4

Working on this, attempt 1.

loop:claim run=contribute-20261030-095313-63 branch=loop/63 attempt=1 issue=63

fix(automations): keep edit-page autosave drafts when the server only changes the active flag

The edit page's autosave key now hashes only the fields the form edits (name, description, lexicon, operations, wantedDids, steps, isBlueprint, dedupeBySource) plus the rkey, so a draft survives the server flipping active (dry-run expiry, rate limits, session loss, redundant-automation sync, the pause toggle). Edit-page drafts are also tagged with a hash of the viewer, so when the editable seed did change, the form finds the old draft and shows "This automation changed since your draft was saved..." with Restore draft and Discard draft buttons, instead of silently dropping it.

What changed #

  • app/islands/form-autosave.ts: saveAutosavedDraft takes an optional scope, stored hashed in the entry. New readOtherAutosavedDraft(scope, context) (newest fresh draft on this path with that scope under another context) and clearOtherAutosavedDrafts(scope, context).
  • app/islands/AutomationForm.tsx: the edit-page context is built from the editable fields, with the viewer as the scope. When there is no draft for the current seed, the form looks for an older one and offers it. Restore applies it, with header values refilled from the current seed as before, and shows the usual "We restored..." notice, header note included. Discard asks to confirm, then removes it. Both put focus on the Name field.

Verified #

  • pnpm run check: passes.
  • pnpm run test: 179 files, 2774 tests pass.
  • New component tests in AutomationForm.test.tsx: a draft restores after a remount with active flipped, and header values still refill. The changed-seed notice shows and leaves the draft in storage. Restore applies the draft and moves it to the current seed, with no confirm on an untouched form. Discard asks to confirm, keeps the draft on "no" and removes it on "yes". Typing replaces the old draft. Another viewer's draft is never offered. The old test "does not restore an edit draft once the automation changed server-side" is replaced by these.
  • New unit tests in form-autosave.test.ts for the scope lookup and the clear: same path only, newest first, expired and unscoped entries skipped, other viewers and paths left alone.
  • Keyboard: the two notice buttons are native <button>s with distinct names. Focus moving to #sub-name after Restore and after Discard is checked in tests.

Acceptance criteria #

Decisions the owner can reverse #

  • Only edit pages get the scope and the changed-seed notice. Blank-form and duplicate keys are unchanged: the blank-form key is out of scope, and the scope-upgrade stash still uses the full seed.
  • Like the held-back blank-form draft, the old draft stays in storage while the form is untouched. The first autosaved change, Restore, Discard or a successful save removes it, so a path keeps at most one draft per viewer. On the current visit the notice stays up from memory until the user acts on it.
  • Discard asks to confirm and Restore only does when the form has changes, matching the held-back draft's buttons.
  • Edit drafts saved before this change used the old key, which included active. They are neither restored nor offered.

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

loop:pushed run=contribute-20261030-095313-63 sha=9ee443c36b1edc232e09716c007fb81765227c27

Merge. The edit page's autosave key now hashes only the fields the form edits plus the rkey, so a server-side active flip no longer strands the draft. When an editable field did change, the same viewer's older draft is offered with a "This automation changed since your draft was saved" notice and Restore/Discard buttons. Header-value refill is unchanged, and check and tests pass.

What I checked:

  • Acceptance criteria, against the code:
    • The edit context in AutomationForm.tsx is [viewerHandle, {rkey, name, description, lexicon, operations, wantedDids, steps, isBlueprint, dedupeBySource}], with no active.
    • The blank-form and duplicate keys, and the scope-upgrade draftContext, are unchanged, as the issue's out-of-scope list asks.
    • There are tests for the active flip (with header refill), for the changed-seed notice, for Restore (focus moves to Name, no confirm on an untouched form, the draft carries over to the new seed), for Discard (confirm no keeps it, yes removes it), for typing replacing the old draft, and for another viewer's draft never being offered.
  • Bug class:
    • I listed every caller of the autosave helpers and hashContext: only AutomationForm uses autosave.
    • I listed the server writers of the automation row (active and pdsStaleSince/indexedAt in sync; steps only in one-off migration scripts). The edit seed holds nothing else that the server changes behind the form's back.
  • Every changed line in form-autosave.ts:
    • otherKeys stays on the same path, skips the current key, and matches the hashed scope.
    • Reads sweep expired drafts. Drafts with no scope (made before this change) are never offered, which pr.md says.
    • Every way out of the notice cleans up storage: restore, discard, the first autosave, and a successful save.
  • Real flow, in a throwaway component test (since removed), on the edit seed with a webhook credential header:
    • I saved a draft, then remounted with the description changed and active flipped. The notice reads correctly, punctuation included.
    • Restore brought the draft back with the header value refilled from the seed and the "Header values are not saved..." note.
    • The return trip (remount with active flipped back) restored the draft straight away, with no notice and one storage entry.
  • UI: the notice buttons are native <button>s. Focus goes to #sub-name after both. Their names don't clash with the held-draft notice ("Restore that draft"/"Discard that draft"), and the stale notice never shows alongside the restored-draft notice.
  • pnpm run check: pass. pnpm run test: 179 files, 2774 tests, all pass.

There is no migration, no follow-up, and the owner left no comments.

loop:verdict run=review-20261030-100231-63 sha=9ee443c36b1edc232e09716c007fb81765227c27 verdict=merge

Merged main into the branch: 55851ef6f3688bc49f0b2848ba2c02862f8b197e.

loop:carry from=9ee443c36b1edc232e09716c007fb81765227c27 sha=55851ef6f3688bc49f0b2848ba2c02862f8b197e

Merged onto main as 2314732f3a8f87f1a48c675eb04bf3eab0b47e16.

loop:merged sha=2314732f3a8f87f1a48c675eb04bf3eab0b47e16 head=55851ef6f3688bc49f0b2848ba2c02862f8b197e

Sign up or Sign in to add to the discussion.