Repository navigation
feat(cli): follow payments live with payments list --watch - #111
Conversation
e5f16f2 to
a06b6e8
Compare
a06b6e8 to
9f5a0a4
Compare
tee8z
left a comment
There was a problem hiding this comment.
Requesting changes on 9f5a0a4 (base #110). The event stream is well formed: piped output, --json and NDJSON write one compact envelope per event, and --output-file appends and syncs each one. The live screen is restored before the stop message, Ctrl-C exits 130, untrusted text is scrubbed, --interval is at least 1 second, and --watch conflicts with --all.
P1: a watch on a saved browser login stops once its organization token expires. watch (src/api.rs:946) keeps sending with the Api built for the first read. Its Authorization holds the organization token from one exchange, which is short-lived (exchange checks its expires_in). When that token expires, every refresh gets a 401, which matches Err(error) => return Err(error) (:991), and the watch exits 3 with "Check the credential with voltage auth status". The PR's own staging example uses --account me, a saved login, while the tests use API keys only. On a 401, or before the token expires, resolve and exchange the login again, and add a test with a short expires_in.
P2: a 5xx without a JSON body ends the watch. The non-JSON error path in Api::send attaches no HTTP detail except on a 403, so http_status() is None and the error kind is Api, not Transport. The retry guard at :986 then does not match, and a gateway's HTML 502 or 503 ends the watch, although the docs say that a refresh failing with a 5xx is retried. The test passes only because its 503 has an empty body. Please add a case such as ResponseTemplate::new(502).set_body_string("<html>Bad Gateway</html>").
P2: a 429 ends the watch, and Retry-After is ignored. The loop sleeps a fixed interval (:969), never reads retry_after, and has no backoff after failures. That goes against quality.md's "Poll only with server-provided retry hints".
P2: only the first page is compared. Each refresh resends the same query. A payment that has fallen off the first page never produces updated, and a burst bigger than a page is never fully reported. --sort-order ASC or --cursor means new payments never show at all. Force sort_key=updated_at and sort_order=DESC, and reject --cursor, --sort-key and --sort-order with --watch, or document the limit.
P2: the live frame has no height limit. With the API's default of 100 items, the header, and the newest marked rows with it, scroll off a 40-row terminal. Trim the frame to the terminal's height.
Nonblocking:
--timeoutdoes not bound a watch, whichrequests.mdand quality.md imply it does, so the exception should be documented.Stopped watching payments.bypasses--quiet.- The PTY test sleeps a real 2.5 seconds, which quality.md forbids.
- The transport-error retry, the 401 path and the screen restore after a non-Ctrl-C error are untested.
Gate and CRAP on this head are running, and I will add the results.
|
Gate results for |
9f5a0a4 to
83aa1aa
Compare
|
Review-body items, in 83aa1aa:
|
83aa1aa to
41daf75
Compare
payments list --watch reads the list again every --interval seconds (default 5) until Ctrl-C, with the same filters. On a terminal it is a live table on the alternate screen that marks payments new or changed since the last refresh; Ctrl-C restores the screen and prints "Stopped watching payments." with exit code 130. Piped or with --json it writes one event envelope per payment: snapshot, then new or updated. A transport or server error is retried at the next refresh; any other error ends the watch.
41daf75 to
5c45231
Compare
tee8z
left a comment
There was a problem hiding this comment.
Approved 5c45231 against feat/cli-look-11-bolt (#110 a0ab94e). My findings are fixed:
- P1, token expiry:
Callkeeps theResolver, and a 401 resolves the credential again once.resolve_organizationexchanges again on every call, with no cached token, so a saved login gets a fresh organization token. The flag resets after each successful refresh, so the watch survives every expiry, and a second 401 in a row ends it.a_watch_on_a_saved_login_exchanges_again_when_its_token_expirescovers it. - HTML 5xx and 429:
Error.response_statusrecords the status without adding an HTTP detail, andwatch::retriescovers transport failures, 429 and any 5xx. The end-to-end test sends an HTML 502 and a 429. - Retry-After and backoff: a failure waits for the server's hint, or else a jittered backoff that grows to 60 seconds and resets on success.
- First page only: every read forces
sort_key=updated_atandsort_order=DESC.--watchconflicts with--cursor,--sort-keyand--sort-orderas well as--all, and the docs state the one-page limit. - Frame height: rows beyond the terminal's height are cut, and the cut is counted.
- Nonblocking notes:
--timeoutis documented as limiting each refresh.--quiethides the stop message. The PTY test waits on a condition instead of a fixed sleep. Screen restore after an error has a test.
Nonblocking, new in this revision: a zero Retry-After gives no pause. retry_after() returns Duration::ZERO for Retry-After: 0, for x-retry-after-ms: 0, and for an HTTP date in the past, which clock skew can produce. A 429 or 5xx carrying such a header is retried immediately, with no pause, for as long as the server keeps answering that way. On a successful refresh the same header overrides --interval, so the watch can poll faster than asked. Retry-After is a minimum wait, so taking the longer of the two would fix it: next.retry_after.map_or(interval, |hint| hint.max(interval)) on success, and hint.max(backoff.pause()) on failure. A unit test or a Retry-After: 0 case in the retry test would pin it.
Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (192 unit, 146 integration) and python3 scripts/check-coverage.py passed. CI is green. I did not run the CRAP report for this re-review.
Stack position: 12 of 13 in the look-and-feel stack. Base:
feat/cli-look-11-bolt(#110).Problem
There was no way to follow payments as they arrive short of rerunning
payments list. Scripts that wanted to react to new or settled payments had to poll and diff the list themselves.Behavior
voltage payments list --watch [--interval SECONDS]keeps reading the list, with all ofpayments list's filters, until Ctrl-C.On a terminal, it shows a live table on the alternate screen, newest first:
●(yellow) marks payments that are new or changed since the last refresh, for one refresh. Statuses are colored as in feat(cli): show friendly amounts and relative times, and color tables #101.Stopped watching payments.with exit code 130, as for any interrupt, without anerror:label.Piped, or with
--json/--output ndjson, it writes one event envelope per payment, one per line:{"outcome":"event","event":"snapshot"|"new"|"updated","id":"…","data":{…payment…}}. The first read givessnapshotevents, and later reads give only new or changed payments. A change means a newstatusorupdated_at.Errors: a transport error or 5xx is retried at the next interval; any other error (such as 401 or 403) ends the watch with its usual exit code.
Naming: the plan called this
voltage payments watch. It is a flag onpayments listinstead, so it reuses that command's filters (--wallet,--statuses,--direction, …), scope, and auth without a second alias mechanism next tosendandreceive. Apayments watchalias can be added later. Keypressqwas left out; Ctrl-C ends the watch.Design notes
ReadThen::Watch { interval_seconds }is planned fromApiInvocation::watch.WatchFlags(--watch,--interval) attach only topayments list:--intervalrequires--watch,--watchconflicts with--all, and the interval is at least 1 second.watch::Seenis a pure function of the pages: it decides snapshot, new, or updated by ID.Output::live_framerenders the table with the feat(cli): show friendly amounts and relative times, and color tables #101 renderer and prefixes each row's marker. A row's line is its payment's index plus one; the header is line 0. TheNext cursorline is dropped from the live view.LiveScreenis a guard that enters the alternate screen and hides the cursor, and undoes both on drop, including when Ctrl-C drops the command. Screen control goes straight to stdout, not through the color stream, which would strip it underNO_COLOR.SubmissionState::watchingletsstartupreport the Ctrl-C calmly.Try it on staging
Setup: Try a pull request on staging (added by #97). Read-only.
Expect: the screen switches to a live table that refreshes every 2 seconds. In another terminal, create or pay an invoice (for example
voltage payments receive --account me --wallet WALLET_ID --currency btc --kind bolt11 --amount 1000 --unit sats --execute), and the new payment appears with a yellow●. Ctrl-C returns you to your normal screen withStopped watching payments.;echo $?prints 130.voltage payments list --account me --watch --interval 2 --limit 5 --json voltage payments list --account me --watch --interval 2 --limit 5 | catExpect: five
"event":"snapshot"lines, then aneworupdatedline as payments change. Ctrl-C to stop.Expect: usage errors (
--intervalneeds--watch;--watchcan't be combined with--all).Validation
cargo fmt --check,cargo clippy --locked --all-targets -- -D warnings,cargo machete --with-metadata,cargo test --locked(171 unit and 134 integration tests), andpython3 scripts/check-coverage.pyall pass.Seen: snapshot, then nothing for an unchanged page, thennewandupdatedin list order; no ID means not followed.--intervalwithout--watch,--watch --all, a 0 interval, and--watchon another list are rejected.snapshotA,newB,updatedA; Ctrl-C gives exit 130 andStopped watching payments.on stderr, alone.src/cli.rsendpoint, up from 18.00 because it now attachesWatchFlags). The newwatchis 8.0 andlive_frame6.0.Docs
docs/guides/payments.md(new Watch payments section) andCHANGELOG.md.