aboutsummaryrefslogtreecommitdiffstats
path: root/src
Commit message (Collapse)AuthorAgeFilesLines
* A gift opened into a new wallet goes on, once its seed is written down, to ↵Adam Soltys14 hours23-18/+80
| | | | | | | the first-run steps: pick a username, then a picture and a cover ("Not now" lands in the account the gift arrived in). Also fixes Verify backup on the gift screen, which crashed (pickConfirm wasn't passed to the gifts feature) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A payment that arrives as several coins is one Received row: a claimed gift ↵Adam Soltys14 hours2-2/+34
| | | | | | | held in two coins (dust padding split the sender's 886 into 595 + 291) showed as two receives. Receives stamp their mailbox message and the history folds a message's coins together; older rows fold on the same delivery instant Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A feed clip with timed reactions seeks on the moments bar alone: the ↵Adam Soltys19 hours3-8/+37
| | | | | | | player's own controls (its seek bar) stay off, the middle play/pause shows whenever it's paused (touch too), and the sound button stays to turn it down again Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* One seek bar under a feed video: the moments timeline shows beneath a clip ↵Adam Soltys19 hours2-0/+4
| | | | | | | only when the post has timed reactions to mark (the player's own bar does the seeking) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A closed wallet's NWC payments fit iOS's ten-second wake: a paid send is ↵Adam Soltys19 hours5-34/+143
| | | | | | | | | | | | | | | never marked for revocation by a second wake, lookups report outgoing sends and finish them, and pushes no longer run side by side PullCap's night of 2026-10-24: every wake's diary stopped 8-12 s after its push (iOS stops the worker there, not at 30 s). Two wakes two seconds apart both drove one send; the second's InitiateLightningPayment was refused ("state: ln-spent") and the paid send went to 'revoking', where every later wake spent its whole budget on it, so no request was answered after midnight. - manager: an initiate refused as ln-spent follows the payment ('initiated'); a send in 'revoking' asks the ASP first (paid: settle; in flight before expiry: back to waiting; not found: revoke) - worker: 9 s budget on Apple endpoints (27 s elsewhere) for answers, earlier sends and renewals; every ASP step held to it - lookup_invoice asks every copy of the connection and drives an outgoing send in flight, answering settled with the preimage - a lease in the worker's IndexedDB store serialises pushes (the Web Lock didn't); diary notes a missing Web Locks API - the reply and the mirror save run together; stuck sends are finished before a new payment only when they hold its coins Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* Feed clips with a mouse: a big play/pause in the middle on hover that plays ↵Adam Soltys36 hours23-4/+107
| | | | | | | or pauses in place, a click on a playing clip pauses it (a paused one still opens the pager), and a tall clip is held to three quarters of a computer screen with its box narrowed to the clip's shape Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A closed wallet finishes the Lightning sends an earlier wake left in flight, ↵Adam Soltys43 hours8-98/+331
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | instead of calling the invoice "already paid" with no preimage and locking its coin until the app opens PullCap's night of 2026-10-23 (hourly 1-sat NWC requests to a closed iPhone PWA): three of fifteen hours went wrong the same way. The push woke the worker, it reserved the coin and wrote the ln-pay action to the mirror, and the phone stopped it before the ASP was ever asked (no cosign in captaind's log for those minutes). The client's retry ten minutes later hit the manager's duplicate check, "this invoice was already paid or is being paid", published as an INTERNAL error that the client read as paid — hours before the money moved. And since the stuck action held the wallet's only coin, the next hour's request (a different invoice) found nothing spendable and fell to "Open Coinos to approve". Everything settled at once when the app opened at 07:12 and resumed the actions. - nwc-respond.js: pay_invoice first looks for a send already on the books for this invoice — finishing one in flight and answering with its preimage, repeating the preimage of one already done (nothing new on the spend log) — and before paying a new invoice drives any other stuck send to completion so its coin comes back. tendLnPays() does the same on every wake, lookups included, and hands back "Payment sent" lines for what settled. rec.pendingPays remembers which connection a send belongs to (the action is the manager's); the app's mirror rewrite carries it. - manager.js: the duplicate check says "already paid" only for a settled send, "already in progress" for one on its way. - features/nwc.js: the open app answers a repeated invoice from the local action (preimage when settled, an honest "still in progress" otherwise) before asking the ASP about a sibling device. - sw-nwc.js: one push at a time over the shared mirror (Web Lock), so a client's lookup + retry seconds apart can't double-drive one send. A diary of what the worker did — each push, each ln-pay step as the mirror saves it, every notification shown or refused, the fallback taken — is appended line by line to IndexedDB (nwc-bg.js appendBgLog), so a worker the phone stops mid-push leaves its last step behind. Settings → Notifications → "Background activity log" copies it. - tests: nwc-sw-test covers the four resumed-send cases; sw-push-quota mocks the new seams and its "suspended app" case now expects the stand-in-first order shipped in 91dbee4 (it had been failing since). Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Amber sign-in on Android works past the first request, and a migrated ↵Adam Soltys2 days4-34/+127
| | | | | | | | | | | | | | profile no longer keeps its released placeholder address Jarryd (DM, 2026-10-24): on Android he could not sign in with Amber and had to paste his nsec, and his profile page showed the temporary name he had picked while the editor showed his real one. Amber: Chrome lets a page open another app only on the heels of a tap. The Sign-in tap carried the get_public_key request, but the request the login fires once the key is back (decrypt the backup, sign the relay list…) had no tap behind it, so Chrome dropped the nostrsigner: navigation with a console warning the page never sees ("Navigation is blocked") and the card spun forever. Reproduced on Adam's Pixel 9 with Amber 6.6.9 (Amber 4.1.3 did not even parse the web request). Now a request without a user activation behind it waits for one: the card says the signer has another request and offers an "Open signer app" button that launches it from the tap; a launch that leaves the page in front (never hidden) is treated as blocked too and gets the same button. The clipboard is still read on return (that part worked). Profile: the migration released the placeholder name and repointed lud16/nip05 to the new one, while the legacy-avatar import ran at the same moment, read the same old kind 0, and republished it nine seconds later with the placeholder address back ([email protected], dead on both coinos.io and v3). Kind-0 publishes now go one at a time, merge over the newest copy this session sent, and are stamped strictly later than it (relays keep the lower id of two with the same second). The profile page shows the claimed address instead of a same-domain name that is no longer ours, and once the domain confirms the old name is dead it repoints the kind 0 in the background (healStaleAddress), so zaps to the published lightning address reach the wallet again. tools/profile-publish-race-test.js plays the migration's two publishes at once against an in-memory relay. tools/amber-login-test.js and identity-test pass; name-migration-test and avatar-profile-test fail the same way on the previous commit. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* The media grid's order holds for the session: clips found later go under the ↵Adam Soltys2 days1-2/+24
| | | | | | | tiles already showing, in their own date order; only a post newer than the top tile goes above it Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* The single-file build boots again: app.js exported its safe-mode helpers, ↵Adam Soltys2 days1-2/+2
| | | | | | | and a classic inline script cannot carry an export (standalone.html threw "Unexpected token 'export'" since b3cee21; nothing imported them) Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* On a phone a live stream is laid out the way Nostrich does it: the picture ↵Adam Soltys3 days2-2/+19
| | | | | | | edge to edge, the chat a plain full-width list under it Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* Switching the unit with an amount typed into the send form converts it: the ↵Adam Soltys3 days1-5/+18
| | | | | | | same money in the new unit, round-tripping exactly Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* On a server that does not honor expired coins the custody tier in force is ↵Adam Soltys3 days1-4/+16
| | | | | | | | | | | | | | | | | | | | | | | | | "self": renewal stays automatic there, in the app and the worker "Coinos holds my coins" was meant to exist only on servers that keep cosigning a coin past its timelock (the presets carry honorsExpired), and the custody card hid it elsewhere and showed "self". But the tier the renewal pass and the worker mirror read came from the stored choice or the custodial default regardless of server, so a wallet on Second or a custom captaind (where an expired coin is gone) renewed nothing and warned of nothing since 648a550 (2026-10-22): no backstop orders, no renewal in the last hours, and the worker registering nothing while closed. getCustody() now returns "self" in place of "custodial" whenever the active server does not honor expired coins ("exit" stays "exit"); the mirror carries the same, so the worker follows. The stored choice is left alone for a later switch back to a coinos server, and choosing a tier always writes it. tools/ark/custody-foreign-test.js: a custom server with no honorsExpired, nothing stored or "custodial" stored, reads as "self", the card offers only self/exit, and the renewal pass registers backstops and renews the coin at its timelock. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A partial on-chain send from Spending takes whatever coins add up to the ↵Adam Soltys3 days1-33/+56
| | | | | | | | | | | | | | | | | | | | | | | | amount; a split that lands as several coins sends them all An offboard moves whole coins, so a partial amount was first split off by a payment to the wallet's own address, and the send then waited for ONE new coin of exactly that size. A payment that needs more than one input arrives as one coin per input, so for any balance held in two or more coins the wait timed out ("The split is taking longer than expected") while the split had gone through — every retry split again and nothing was ever sent (Jarryd, 2026-10-23: three splits of 185,000 sats, each landing as 109,958 + 75,042). Now the coins that leave are chosen as a set: a single coin of the exact size, else the free coins that already add up to it (largest first, so the halves an earlier split left behind go as they are, with no new split), else the split's own output — every new coin on the receive key that adds up to the amount. Change keeps its own key index and stays. Same carving for the automatic withdrawal. tools/ark/offboard-split-test.js covers the four cases against the real send form. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* Push handler: a notification the platform refuses is retried plain and never ↵Adam Soltys3 days1-4/+15
| | | | | | | | | | | | | | | counts as shown PullCap's iPhone paid four times overnight with the "payments sent" notifications on and never showed a "Payment sent" line. The worker hands the handler the heads-up (covered by a test now); the handler flagged the push as answered BEFORE calling showNotification, so a refusal left the push with no line at all — on iOS a silent push, a strike towards losing the subscription. Now the flag follows success, a refused notification is retried with the plainest options, and the stand-in covers the rest. iOS never replaces by tag, so renotify is left out there. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Deep coins are renewed under every tier, free of the base fee, before the ↵Adam Soltys3 days5-5/+93
| | | | | | | | | | | | | | | | | | | server's depth cap strands them A coin's exit depth grows by one per off-chain hop; the server refuses to cosign a spend of a coin at max_vtxo_exit_depth (100), so a wallet paying hourly from one change chain has an unspendable coin within days. Survey of the ASP's 2,279 unspent coins: median depth 7, 154 coins past 40 holding 7.7k sats, five already stuck at the cap. The server now waives the refresh base fee when every input is at its deep_waiver_depth (60; ArkInfo.fees.refresh carries it). The client mirrors that in refreshFee, never selects a capped coin as an input (the shortfall message says why), and flattens deep coins whatever the custody tier: the open app in a round of their own, the worker with a standing order two blocks out per coin. The default tier's copy names the one exception. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* iPhone: a push shows its line first and the outcome replaces it; a crash ↵Adam Soltys3 days5-2/+98
| | | | | | | | | | | | | | | | | | | | | | | | | | | loop on open lands in safe mode; a closed-app payment is one history row PullCap's night of 2026-10-22 on an iPhone: four hourly payments made while closed, no notification for the fourth, then no more wake-ups until the app was opened — WebKit's silent-push quota. APNs accepted every push (no 410); the device had stopped listening. The stand-in line went up at the END of the push handler, after a payment that can take most of the budget, and a worker stopped before its last line showed nothing. On an Apple endpoint the line now goes up first and the outcome ("Payment sent") replaces it. The same phone then crashed on every open ("A problem repeatedly occurred"). An inline guard counts page starts before the app runs; the app clears the count after eight seconds alive or a clean leave, and notes the boot phase it reaches. A third start in a row opens in safe mode: no automatic Ark connection, a notice naming the last phase, "Open normally" and "Copy report". Every payment the worker made showed twice once the app opened: the worker's own ln-send row (merged from the mirror) and the "while closed" note the app adds from its spend log. The note now stamps the existing row, and lnFoldedIds hides a twin that still forms. The balance was never counted twice — it comes from the coins. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* The wallet opens on the Ark server's last published info when the server is ↵Adam Soltys3 days2-4/+36
| | | | | | | | | | gone, and a unilateral exit needs only that Adam, 2026-10-23: the app must stay navigable and able to exit if Coinos disappears. The shell already was (the worker precaches every chunk; verified with every coinos host unresolvable: boot, wallet creation, Savings address, Send all work, and Electrum fails over to public servers). The Ark side was not: connectArk's init asked the server for its info and threw, so the Manage page's Exit stayed disabled and doArkExit waited on a connect that could never finish. ArkManager takes infoHint (the btc-wallet-arkinfo cache the app keeps per server): when GetArkInfo fails and a hint exists, init proceeds on it with `offline` set; the server key, fee tables and lifetimes are in it, and an exit needs the chain (esplora, not Coinos) rather than the server. doArkExit uses the initialized manager and waits on init alone, not on the Nostr snapshot gate the full connect also waits for (its relays may be gone too). tools/ark-offline-init.test.js. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Automatic renewals only when worth it (fee under a tenth of what renews); ↵Adam Soltys3 days3-5/+17
| | | | | | | | | | orphan HTLC coins are settled by the server's answer Adam's wallet renewed a 1,000-sat coin past its expiry plus four bits of change for 301 sats — the expiry trigger had no floor, only the depth trigger did. Now every automatic renewal, under either renewing strategy, is skipped when the fee would exceed a tenth of the coins it renews; they stay spendable, the server honors them, and the strategy card says so. Reconcile: five 22-sat HTLC-send coins sat 'pending' for days — their Lightning payments' actions had been pruned, and an unheld pending coin was only asked about when it was a plain pubkey coin. HTLC orphans are asked about now too: spent is spent; one the server still holds open stays pending (an HTLC coin is never this wallet's to spend). tools/ark-htlc-orphan.test.js. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Custody strategy: the price sits under the drop-down, not in the option text ↵Adam Soltys3 days1-1/+4
| | | | | | (which overflowed on a phone) Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Manage page: the card is "Custody strategy"Adam Soltys3 days1-1/+1
| | | | Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Manage page: Renew, Withdraw and Exit sit under the coin table and act on ↵Adam Soltys3 days2-53/+58
| | | | | | | | the ticked coins; a unilateral exit can be partial; the explainers moved to the confirmations Adam, 2026-10-23: the three buttons belong right below the table so it is clear they act on the selection, "co-operative exit" is Withdraw and "unilateral exit" is Exit, Exit names the ticked total and takes just those coins, and the explainer cards were a wall — they now appear where the action is decided: the renewal text and fee schedule on the Renew confirmation, the withdraw note on the form it opens, the exit cost comparison on the Exit confirmation. The page is the coins, their three buttons, and the custody strategy. The buttons stay (disabled) while every coin is in a round. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Co-operative exit button: the amount in sats or BTC, never fiatAdam Soltys3 days1-3/+7
| | | | Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Spending settings: the coins first, then the custody strategy as a drop-down ↵Adam Soltys3 days2-12/+11
| | | | | | | | that describes only the chosen one Adam, 2026-10-23: three descriptions at once were a wall of text. The card now sits between the coins table and Renewal, is a select of the three strategies with their prices, and shows one paragraph — the chosen strategy's. The intro is two sentences shorter. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Spending settings: the init-time cache write keeps the last tip seen instead ↵Adam Soltys3 days1-1/+6
| | | | | | of zeroing it for the beat before the tip arrives Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Spending settings: ask for the chain tip right after init, so expiries and ↵Adam Soltys3 days1-0/+8
| | | | | | exit costs are judged at once and the next refresh starts from the last tip seen Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Spending settings paint their real numbers at once after a refresh: the ↵Adam Soltys3 days1-10/+42
| | | | | | | | | | manager is readable the moment its own init is done, and the server's fee table and tip are cached per server Why: the coins were on screen at 350 ms (from the store), but the renewal price read "free", expiries "—" and exit costs the fallback for eleven seconds, because the pages read everything from `ark`, which is only set once the Nostr snapshot restore has also finished (Promise.all with mgr.init) — a gate that exists so the mailbox isn't replayed before the snapshot's cursor arrives, not for display. GetArkInfo itself answers in 0.4 s. `arkInited` is the same manager as soon as mgr.init() resolves (info, server key, tip, persisted coins); arkView() = ark || arkInited is what the custody card and the coins page read from. Renew (the one action the page runs on the manager directly) still waits for `ark`; exits go through connectArk, which waits anyway. For the first half second before init, btc-wallet-arkinfo:<server> in localStorage carries the last info and tip seen (tip advanced a block per ten minutes), so the first paint prices renewals right and shows days-left; a card with no info at all shows "…", never "free". Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Custody card: drop "Coinos holds it for you"Adam Soltys3 days1-1/+1
| | | | Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Custody card: a full stop where the colon wasAdam Soltys3 days1-1/+1
| | | | Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Custody card: "paying still works" (paying and withdrawing are one thing; ↵Adam Soltys3 days1-1/+1
| | | | | | receiving goes without saying) Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Co-operative exit takes the ticked coins: a subset on the Manage page goes ↵Adam Soltys3 days2-22/+40
| | | | | | | | on-chain as exactly those coins Adam, 2026-10-23: the checkboxes only fed Renew; the exit button always prefilled the whole balance. The manager's offboard always took a coin list, and the fee quote prices each coin by its own expiry, so the page now passes the selection through: the card says which coins will move and prices their total, the exit form shows "N selected coins · amount" instead of an amount field, the quote is for those coins, and no fresh coin is split off first. Nothing ticked (or everything) still means the whole balance, with the editable amount as before. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Custody card: plain words — "Allow coins to expire", "Minimize renewal ↵Adam Soltys3 days1-10/+10
| | | | | | | | cost", "Minimize exit cost"; no timelocks, rounds or "the old Coinos" Adam, 2026-10-23. Coins "expire"; the exit is "yours until then"; what a renewal buys is said without naming the mechanism. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Custody card copy: "coin", not "Spending coin"; Coinos capitalized; the ↵Adam Soltys3 days1-7/+7
| | | | | | | | default tier says the exit stays until a coin's timelock runs out Adam, 2026-10-23: "Coinos holds my coins" read as if the holder never had a unilateral exit. They do, until the timelock runs out; only then does Coinos hold the coin. The option is now "Coinos holds my coins once they expire", its description says so, and the intro says what the timelock buys and what renewing keeps. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Zap sound: shorter and deeper — the strike alone, 0.35 s, 20% lower, ↵Adam Soltys3 days1-1/+1
| | | | | | | | | | low-passed at 2.6 kHz Adam auditioned twelve takes on the Jacob's ladder clip (freesound 536741) and chose this one: the first 0.45 s of the arc with its opening spike cut (0.10–0.45 s), pitched down 20%, the fizz above 2.6 kHz removed, a 130 ms fade. Same loudness as before (loudnorm −16 LUFS). The clip chunk shrinks from 12 KB to 4.7 KB of base64. tools/zap-feedback-test.js pins the new length (0.3–0.5 s) and dispatches the held-screen's two clicks in the page: the sound toggle re-renders that screen, and puppeteer's own click found its node detached mid-morph (failed on HEAD with the old clip too). Co-Authored-By: Claude Fable 5.1 <[email protected]>
* The third custody tier keeps exits cheap by depth, not by date: "Keep my ↵Adam Soltys3 days3-19/+51
| | | | | | | | | | exit cheap" renews a coin once its history is four hops deep Why: a unilateral exit publishes one on-chain transaction per off-chain hop, so depth — not age — is what makes an exit expensive, and a renewal a week before expiry flattens exactly the same history a renewal in the last hours does. The week-ahead "early" tier only guarded against the server being unreachable at the very end, which is not the case worth selling. (Adam, 2026-10-23.) exit = self (last-hours renewal, free bracket + base, the same grid-snapped backstop) plus: any coin whose genesis chain is 4+ hops deep is renewed in the next round the app runs, paying the top bracket + base — but only when that fee is under a tenth of the coin (flattening a 21-sat coin for 300 sats helps nobody; its exit is uneconomic anyway). Deep coins ride along with an expiry renewal when one is due, or get a round of their own. The server-side backstop stays near-expiry only: depth changes only while the wallet transacts, i.e. while the app is open. A stored 'early' reads as 'exit'. Card copy says what the hop count means and what the two prices are for. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Custody is a choice, and coinos holding the coins is the default: nothing ↵Adam Soltys3 days7-119/+340
| | | | | | | | | | | | | | | | | | | | renews on its own and no renewal fee is paid unless the user opts in Why: renewal fees were "money passively draining" to people who thought they had a custodial wallet (Stacker News, 2026-10-22). Measured over Sep–Oct: 84% of what users paid came from the client's own consolidation round at 20 coins (0.2% + 300 on every fresh coin), the rest from 300-sat rounds that each cohort of Lightning-received coins (30-day pool lifetime) triggered on its own schedule. The ASP itself honors a coin past its timelock — rounds and offboards never checked expiry, and captaind's allow_expired_arkoor (now on at ark.coinos.io) lets arkoor spends through too — so expiry only ever meant "the exit is gone", which most people coming from v2 never had. Settings → Spending gets a Custody card with three tiers: - coinos holds my coins (default): no renewals, no fees; a coin past its timelock shows as "held by coinos", stays spendable and receivable, and is left out of a unilateral exit (there is nothing on-chain for it to take). - Keep my own exit: renew in the last hours (free bracket + base), the server backstop registered 36 blocks out and snapped to a 48-block grid so every wallet's orders share one round. - Renew a week early: 1008 blocks out on a 144-block grid (0.1% + base). renewPolicy() in ark/manager.js is the one table; the app, the service worker (the mirror carries the tier) and the Manage page all read it. Only shown on servers that honor expired coins (presets carry honorsExpired); elsewhere renewal stays automatic. Manager: honorsExpired makes a past-timelock coin ordinary money (balance, selection, receive validation via acceptExpired) while a server refusal (expiryRejected) still excludes it; such coins spend LAST so an older build on the receiving side almost never meets one. A once-per-wallet rescan re-reads the mailbox and adopts coins older builds refused as expired, un-forgets expired dust, and clears stale refusals. The refresh gate that retired a renewal of expired inputs is gone (the server accepts them; the Manage page's Renew of exactly those coins was failing silently). Automatic consolidation is gone; the send cap goes 24 → 48 coins; small change (<1,000 sats) folds into a renewal that happens anyway. Server (cs:~/ark/captaind-main.toml, restarted): allow_expired_arkoor = true; pool vtxo_lifetime 4320 → 8640 so Lightning-received coins live as long as the rest. Tests: tools/ark/custody-test.js (policies, honoring, ordering, grid snap, validator), tools/ark/custody-ui-test.js (the card, held coins, exit count, switching), nwc-sw-test updated for the tier the worker reads. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Media grids: one tile for a clip uploaded twice, and a picture that won't ↵Adam Soltys4 days2-10/+36
| | | | | | | | | | | | | | | | | | | | | | | | load stays out - Videos: one person's clips of one size to the byte are one clip. Beatbox Serenade posted the same video as a note and as a video event, each upload a fresh Blossom hash, so neither the file nor the words matched. Only a size the post or the host states counts (a bitrate x duration guess is marked sizeGuess and never matched). - Images: a picture whose load failed was hidden by a class the next repaint rewrote, so the broken-picture glyph came back (a deleted nostr.media file in #artstr). Failed pictures are now remembered for the session and left out of the grid. - A tile fetching its own frame froze the whole tile against repaints, so one built for the one-to-a-row layout never got its --ar in justified rows (zero width). Only the player and its stand-ins are frozen now; the render draws the cost badge itself. Tile pictures no longer size their tile (a frame-grab <video>'s 320px stretched its row). - Test fixtures that served one file under several names (or one imeta size for every clip) now differ per clip. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01K5xtiFrkFVsrkWx6JDQjiT
* Videos grid: a clip the player can't load leaves the grid instead of sitting ↵Adam Soltys4 days1-0/+12
| | | | | | | | | | | | | there as a blank tile A deleted file on Primal's blossom answers 404 without CORS headers, so the probe saw only a quiet host (not a 404), the server had no still for it, and the tile's fallback player failed with nothing listening: a blank box (Adam's iPhone). The player's error now hides the tile and marks the clip gone for a day, as a 404 the probe could read already did. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01K5xtiFrkFVsrkWx6JDQjiT
* iPhone: a push subscription iOS dropped is made again when the app opens or ↵Adam Soltys4 days1-3/+21
| | | | | | | | | | | | | | | comes back WebKit removes a subscription after three pushes that showed no notification (743011c stops new strikes, but a wallet already struck out stayed deaf): background stayed on, getSubscription() came back empty and refreshRegistration gave up, so NWC requests stopped waking the closed app until the switch was toggled (reported by PullCap). It now subscribes again (also when the VAPID key changed) and re-registers with the notifier, on start and whenever the app returns to the screen. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01K5xtiFrkFVsrkWx6JDQjiT
* Videos: a desktop-sized window defaults to every shape (a phone keeps to its ↵Adam Soltys4 days1-5/+19
| | | | | | way round; the choice is stored only when it differs from the device's default). A clip's shape falls back to its still's proportions, so leaving All shapes finds the clips known. The grid's pager keeps a clip of unknown shape out until measured, as the grid does (d898efb let an unmeasured landscape clip into a phone's vertical pager). Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Images grid in justified rows too: tiles in each picture's own shape (the ↵Adam Soltys4 days3-15/+37
| | | | | | tile thumbnail is no longer a square crop: 360px tall, its middle cut to 1:2–2:1; vmedia pic.webp?s=row for hosts without CORS; tile cache v2) Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Videos grid, justified rows: a tile whose clip's size isn't known yet takes ↵Adam Soltys4 days1-2/+16
| | | | | | its still's proportions Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Videos grid, "All shapes": justified rows (each tile its clip's own ↵Adam Soltys4 days2-7/+20
| | | | | | proportions, every row one height filling the width) instead of squares Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Videos grid: "All shapes" lays the clips out three across in squares (tall ↵Adam Soltys4 days2-4/+8
| | | | | | tiles stay for a phone held upright showing tall clips only) Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Videos grid on a wide screen (wide clips only): one clip to a row with its ↵Adam Soltys4 days2-1/+7
| | | | | | 16:9 poster whole, instead of portrait slivers three across Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Videos: "All shapes" in a pager opened from the grid brings in the clips the ↵Adam Soltys4 days1-3/+6
| | | | | | other way round (the pager now holds every clip, pagerFit keeping the wrong-shaped ones out of sight until asked) Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Videos grid: a clip whose host says it's deleted (404/410) is left out for a ↵Adam Soltys4 days1-5/+12
| | | | | | day instead of sitting there as a blank tile Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Media grid shows a clip or picture once however many posts carry it (same ↵Adam Soltys4 days1-5/+34
| | | | | | file or Blossom hash, or the same words from the same person re-uploaded); on a big window a small clip in the pager is drawn at most 1.5x its pixels instead of blown up across the monitor Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Videos on a desktop: moving the mouse over a playing clip brings the seek ↵Adam Soltys4 days1-2/+13
| | | | | | bar up, and it fades about 2 s after the mouse goes still (it stays while the pointer rests on the bar) Co-Authored-By: Claude Opus 5.5 <[email protected]>
* iPhone: every push ends in a notification, so iOS stops dropping the ↵Adam Soltys4 days1-14/+56
| | | | | | | | | | | | | | | | wallet's push subscription WebKit allows no silent pushes: a push that shows no notification is a strike (even with the app open), and after three it removes the subscription, so a closed wallet stopped waking for NWC requests until it was opened again (reported by PullCap). On an Apple endpoint, the paths that used to end quietly (lookups answered, another device took the request, DMs/reactions we don't announce, an open window, unknown pushes) now show a silent self-replacing "Coinos" line, closed at once when the app is on screen. A backgrounded iOS app is suspended, so an NWC request is no longer handed to it: the worker answers itself. Co-Authored-By: Claude Opus 5.5 <[email protected]>