<feed xmlns='http://www.w3.org/2005/Atom'>
<title>coinosv3.git/tools, branch fdroid-v1.9</title>
<subtitle>coinos v3 — bitcoin, Ark, Lightning and nostr wallet (web + Android)</subtitle>
<id>https://git.coinos.io/coinosv3.git/atom?h=fdroid-v1.9</id>
<link rel='self' href='https://git.coinos.io/coinosv3.git/atom?h=fdroid-v1.9'/>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/'/>
<updated>2026-10-04T15:58:34Z</updated>
<entry>
<title>Ark: a double-tapped zap no longer spends one coin twice</title>
<updated>2026-10-04T15:58:34Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-04T15:58:34Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=7421218e987b0053f1ddbefaddc2c9ecfd664954'/>
<id>urn:sha1:7421218e987b0053f1ddbefaddc2c9ecfd664954</id>
<content type='text'>
A reconcile pass that began before the first zap took its coin handed
it back as spendable (the server still said so) while the zap was in
flight; the second zap picked it and the server refused: 'vtxo …
already spent'. Coins whose state changed during the pass are left
alone. And a payment refused for an already-spent coin now reconciles
and pays once more instead of failing with the raw error.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
</content>
</entry>
<entry>
<title>Renewing asks first, and a renewal links to its round's transaction</title>
<updated>2026-10-03T23:05:42Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-03T23:05:42Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=202dced76eb8617c094686ef86caa979c0074320'/>
<id>urn:sha1:202dced76eb8617c094686ef86caa979c0074320</id>
<content type='text'>
- The coins page's Renew button opens a confirmation instead of renewing
  at once: how many coins, their amount, the fee, and what is left to
  spend meanwhile — with a red notice when that is nothing (the button
  selects every coin by default, which is how a user locked their whole
  balance for an hour on 2026-10-03). It says the coins can't be spent
  until the round's transaction confirms and that a renewal can't be
  cancelled; the wallet renews for free near expiry anyway. The inline
  notice from 328f2db moves into it.
- A renewal keeps its round's transaction id as soon as the round has
  run (before it confirms): the in-progress card on the coins page links
  to it, and the finished renewal's history row carries it, so its
  detail shows the id with View/Copy like any on-chain movement.
  Renewals finished before this change have no id recorded.

tools/ark/renew-confirm-ui-test.js, tools/ark/renew-txid-test.js

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
</content>
</entry>
<entry>
<title>A Lightning payment that can't be covered says why, not 'Payment failed'</title>
<updated>2026-10-03T23:00:39Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-03T23:00:39Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=328f2db9af46cda473318bb3eada0f6e83bb42f1'/>
<id>urn:sha1:328f2db9af46cda473318bb3eada0f6e83bb42f1</id>
<content type='text'>
A user renewed all ten of their coins (55,611 sats) from the coins page,
tried to pay a 55,300-sat invoice three minutes later, and was told only
'Payment failed'. Nothing had failed: the renewal's round ran at once,
but its coins stay unspendable until the round's transaction confirms
on-chain, and that block took 64 minutes. The quote found no spendable
coin and gave up with the generic message; nothing reached the server.

- The quote now tells apart (lnShortfall): money held by another payment
  (wait, as before — now by sum, not one coin covering it all), money a
  renewal holds ('Nothing was sent. N sats are being renewed…'), and a
  real shortfall ('needs X sats with its fee, Y available'). Coins leaving
  in an offboard or exit are never waited for.
- The manager's 'insufficient ark balance' names a renewal too.
- The coins page says before the tap that renewed coins can't be spent
  until the round confirms, and that renewing every coin leaves nothing
  to pay with meanwhile.

tools/ark/ln-shortfall-test.js

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
</content>
</entry>
<entry>
<title>Video frames from bytes, not from a CORS player; pager clips load plainly again</title>
<updated>2026-10-03T22:52:38Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-03T22:52:38Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=141779ab97efad6abefaec0545bba5330fd1e12d'/>
<id>urn:sha1:141779ab97efad6abefaec0545bba5330fd1e12d</id>
<content type='text'>
Regression from 6488806: pager clips were loaded crossorigin so their
frame could be kept, and on a phone the large clip's slide went black.
Some hosts (Primal's R2 bucket) send CORS headers only to a request with
an Origin and don't Vary on it, so a copy the browser cached from a plain
player load answers a later CORS load without them, and it fails. The
same thing made the size probe fail for any clip that had played in the
feed.

- Pager clips load plainly again; the still under a tap-to-play cover is
  restored on every return.
- The probe (and its HEAD) go past the browser cache (no-store).
- A clip's frame is now made from bytes: the opening of the file (index
  plus about 1 MB, no-store) decoded from a blob and kept as a JPEG —
  for tiles and as the pager's poster. No CORS player anywhere. Files
  whose index trails, or hosts whose bytes can't be read, keep the plain
  player still (shown, not kept).
- Kept frames are 480 px wide (were 270) and capped at 600.
- Probe failures are stored as failedAt, so earlier failures are asked
  again at once.
- Test: an R2-style host, its clip cached by a plain load first, is
  still measured and its frame kept (failed before this change).

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
</content>
</entry>
<entry>
<title>Pager: a large clip keeps its picture when you come back to it</title>
<updated>2026-10-03T22:41:42Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-03T22:41:42Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=6488806b840c009278f4ae16de9e079ea4b892bc'/>
<id>urn:sha1:6488806b840c009278f4ae16de9e079ea4b892bc</id>
<content type='text'>
Leaving a slide lets go of its file, and coming back skipped the still
because the cover was already there — black again. The still is now
restored on every return. Pager clips also load CORS-readable (a plain
retry for hosts without CORS), so their frame is kept the first time —
the still under a cover and any clip that plays — and from then on the
poster alone covers it.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
</content>
</entry>
<entry>
<title>Pager: a large clip shows its frame under the tap-to-play cover</title>
<updated>2026-10-03T22:36:58Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-03T22:36:58Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=7adb8cdf7b1a405c58ad9c51c901d852c29dd2ad'/>
<id>urn:sha1:7adb8cdf7b1a405c58ad9c51c901d852c29dd2ad</id>
<content type='text'>
The slide's poster was fixed when the pager was built, and a clip over
budget held no source, so a clip whose frame was kept a moment later (or
never) was a black screen with a button. Now the kept frame is looked up
as a slide comes near; without one, the clip's first frame is loaded
under the cover (only the start of the file, when it leads with its
index or is served in ranges) and kept for next time. A tap plays it
from the start.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
</content>
</entry>
<entry>
<title>Video tiles: a frame even when the size check fails; retry it later</title>
<updated>2026-10-03T22:32:41Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-03T22:32:41Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=d39a1b87358965bb0118cb88e4c8a86c89992a4c'/>
<id>urn:sha1:d39a1b87358965bb0118cb88e4c8a86c89992a4c</id>
<content type='text'>
Efrat's clip (blossom.primal.net → r2a.primal.net) showed a blank tile on
a phone: the 64 KB probe failed there (slow connection), the failure was
kept for good, and a tile with a failed probe never tried for a frame.

- A failed probe is dated and asked again after an hour (it was kept
  forever; old 'failed: true' entries are retried). The probe waits 10 s.
- A tile whose probe failed still tries for a first frame, giving up and
  letting go of the file after 8 s.
- Primal's CDN lets scripts read the bytes but not Content-Range, so the
  size was unknown: a HEAD's Content-Length (readable by any page) fills
  it in.
- No size in imeta but a bitrate and duration (Primal posts those): the
  size is estimated from them. 'dim 1080.0x1920.0' is understood.
- A tile with nothing to show is dark, like a video, not a pale gap.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
</content>
</entry>
<entry>
<title>Media views keep their own cache and crawl for media directly</title>
<updated>2026-10-03T22:05:52Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-03T22:05:52Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=8edbd7b7544d829f44d36b430c4fcbbc521224a8'/>
<id>urn:sha1:8edbd7b7544d829f44d36b430c4fcbbc521224a8</id>
<content type='text'>
Videos was slow because the grid read from the feed's page of posts:
the feed cache holds 30, so after every reload the grid re-crawled the
relays, ten posts a step, each post first waiting (up to 8 s) for its
avatar, pictures and link cards — none of which the grid shows.

- Each feed now keeps a media index (IndexedDB, src/media-cache.js): the
  posts carrying pictures and those carrying clips, 400 of each, and how
  far back it has looked. The grid and pager read from it; a reload
  paints them at once.
- The crawl reads older posts straight into the index, 100 at a time,
  without the readiness wait and without touching the feed's own page;
  it resumes from where it stopped. The bottom of a feed is looked for
  again after a day. Curated feeds still page the usual way.
- A tile's first frame is kept (a small JPEG in the Cache API, when the
  host lets its pixels be read) and painted as a picture from then on: in
  the grid, as the pager's poster and the feed's.
- Probe results kept for 2000 clips (was 600).
- Forgetting a wallet wipes the index and the frames.

tools/feed-media-cache-test.js

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
</content>
</entry>
<entry>
<title>Videos opens on a grid of thumbnails; the pager waits for a tap</title>
<updated>2026-10-03T21:49:57Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-03T21:49:57Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=09ce0c7f213802757b13501898c02cfe309bfbf8'/>
<id>urn:sha1:09ce0c7f213802757b13501898c02cfe309bfbf8</id>
<content type='text'>
Choosing Videos no longer jumps into the full-screen pager: it shows
the vertical clips as a grid, like Images, and a tile opens the pager
on that clip.

Every tile gets a first frame when that costs only the start of the
file (the index leads, or the server serves ranges), whatever the
clip's size; a clip over budget wears its cost as a badge. Only an
index-last file from a server without ranges shows its cost instead.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
</content>
</entry>
<entry>
<title>Videos: know what a clip costs before fetching it; vertical clips only</title>
<updated>2026-10-03T20:38:13Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-03T20:38:13Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=347deec6465a42f24154457fb05227418ee83385'/>
<id>urn:sha1:347deec6465a42f24154457fb05227418ee83385</id>
<content type='text'>
- What a clip costs is learned up front: the post's imeta (size, dim,
  image) when it says, else one 64 KB ranged request — the total from
  Content-Range, whether ranges are served, whether the MP4's index
  (moov) comes before its data, and the picture size from the track
  header. Up to three at a time; remembered across sessions. It also
  replaces the player-based shape probe the feed ran on every clip.
- Clips over 25 MB (8 MB on 2G/3G, 3 MB with data saver), above 1080p,
  index-last files over 8 MB from servers without ranges, or clips that
  couldn't be measured wait for a tap: a cover saying what they cost
  ("50 MB", "4K"). Settings → Nostr → Videos: Autoplay large videos.
- The pager loads only the clip on screen in full; the next gets its
  opening if within budget; everything else holds nothing. Sizes are
  asked three slides ahead.
- Feed clips fetch nothing until they're about to play (preload none),
  with the post's still as a poster.
- Video grid tiles show a first frame only for small clips that start
  quickly; others show their cost.
- Videos lists only clips known to stand taller than wide; one of unknown
  shape is measured and joins when it qualifies; choosing Videos with none
  known yet opens the pager on the first that turns up.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
</content>
</entry>
</feed>
