State diagrams

Item lifecycle

stateDiagram-v2
  [*] --> inbox: saved (glow +1)
  inbox --> reading: opened and scrolled past 5%, or first highlight
  reading --> reading: progress saved while scrolling
  reading --> done: Mark as done (glow +10, finished_at)
  inbox --> done: Mark as done
  done --> reading: reopened manually
  inbox --> archived
  reading --> archived
  done --> archived
  archived --> inbox: restore
  archived --> [*]: delete

Highlight delivery (per destination)

The ledger row is the only state. A highlight with no delivered row for a destination is pending.

stateDiagram-v2
  [*] --> pending: highlight created in a collection with a destination
  pending --> delivered: client wrote it and acked, or server push succeeded
  pending --> failed: write failed (error stored, attempts + 1)
  failed --> delivered: retry succeeds
  failed --> failed: retry fails (attempts + 1)
  delivered --> delivered: re-ack is a no-op (idempotent)
  delivered --> [*]: highlight deleted (cascade)

Moving an item to a collection with a different destination makes its highlights pending for that destination only; they stay delivered for the old one.

Session

stateDiagram-v2
  [*] --> anonymous
  anonymous --> oauth_pending: /auth/:provider/start (state + PKCE cookie, 10 min)
  oauth_pending --> anonymous: state mismatch or exchange failed
  oauth_pending --> signed_in: callback ok, session row + cookie (30 days)
  anonymous --> demo: POST /auth/demo (sandbox user + seed, 3 day session)
  signed_in --> anonymous: logout or expiry
  demo --> anonymous: logout, expiry, or nightly purge
  signed_in --> [*]: delete account (cascade)

Extension selection toolbar

stateDiagram-v2
  [*] --> idle
  idle --> toolbar: mouseup / shift keyup with a non empty selection outside inputs
  toolbar --> idle: click elsewhere, scroll, Escape, selection collapsed
  toolbar --> saving: colour swatch or Note
  saving --> idle: painted, toast "Highlighted"
  saving --> note_editor: Note was chosen
  note_editor --> idle: Save (Cmd+Enter) or close
  toolbar --> answering: Explain / Define / Ask
  answering --> answering: chunks stream into the card
  answering --> idle: close
  toolbar --> idle: Copy (citation on clipboard)
  idle --> mark_card: click on an existing mark
  mark_card --> idle: recolour, note, copy, delete, close

The toolbar never calls preventDefault on copy, and pressing a toolbar button prevents only its own mousedown so the page selection stays alive.

AI request

stateDiagram-v2
  [*] --> quota_check
  quota_check --> rejected: over daily limit (429)
  quota_check --> streaming: Workers AI stream opened
  quota_check --> unavailable: model error (502)
  streaming --> done: [DONE]
  streaming --> aborted: user closed the card or asked a follow up
  rejected --> [*]
  unavailable --> [*]
  done --> [*]
  aborted --> [*]

Glow and streak

stateDiagram-v2
  [*] --> unlit: no activity today
  unlit --> lit: any rewarded action today
  lit --> goal_met: glow today >= daily goal
  goal_met --> unlit: midnight in the reader's time zone
  lit --> unlit: midnight
  note right of unlit
    Streak counts back from today, or from yesterday
    if today has no activity yet, so the lamp
    stays on until the day is over.
  end note