aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
* names: GET /hats/worn lists every head wearing a hat (a couple of dozen), so ↵Adam Soltys36 hours1-0/+13
| | | | | | | coinos holds the whole list and a face it has never seen gets its hat in the first paint instead of after a per-head lookup Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TUeqTyZB8eXLGymUY
* nwcpush: scheduled posts. POST /schedule holds a signed kind-1 note whose ↵Adam Soltys41 hours1-0/+94
| | | | | | | created_at is in the future (2 min to 90 days, 50 per author, 64 KB) and publishes it at that time to the author's relays plus ours, retrying with backoff for up to a day; GET /schedule/status?ids= says which are still held; POST /schedule/cancel takes a kind 5 by the same key and drops them before they reach any relay. Kept in data/scheduled.json Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TUeqTyZB8eXLGymUY
* nwcpush: POST /decline holds a worker's "wait" reply and sends it only if ↵Adam Soltys3 days1-1/+33
| | | | | | | | nobody answers the request in afterMs (5–60 s); NWC wake pushes live 600 s at APNs instead of 120 so a phone on a patchy signal gets the wake late rather than never A worker that can't act on a request (mirror short of keys or coins) wakes the user and, through this, tells the other app to look the request up later — PullCap read the silence as "no answer" and suspended the subscription (2026-10-26). A sibling device or the woken user may still pay inside the window; their real reply wins and the error never lands on a payment. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* names: /admin/parked — deliver a parked payment to the name its owner ↵Adam Soltys3 days1-0/+31
| | | | | | | holds now, or write it off (state.writtenOff keeps the record) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* names: a payment for a released name with no key to follow is parked, not ↵Adam Soltys3 days1-1/+4
| | | | | | | retried (and paged on) forever Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* names: nothing goes out that didn't come in, and no key gets more than 1.5M ↵Adam Soltys4 days1-11/+33
| | | | | | | | | | | | | | an hour without a person - forward() refuses (parks) any delivery not backed by this node's own invoice for that payment hash, paid, for at least the amount — whatever path asked (settle loop, retry tick, /admin/forward) - a key past 1.5M sat delivered in the trailing hour has its next deliveries wait in the queue (ALERT line in the log; the desk monitor pages on a queue older than 30 min) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* names: a float send that threw is looked up before the payment is tried againAdam Soltys4 days1-2/+20
| | | | | | | | | | | | The float wallet keeps every send as an action. forward() records the send it is about to make (destination, amount, start time) before making it; a later try finds that action: done = delivered, failed or never started = go again, anything else = still unresolved, wait. Before, a send that threw after its cosign landed (the coins moved, the answer didn't) was queued and paid a second time. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* names: one payment, one delivery — an idempotency record and a per-payment ↵Adam Soltys4 days1-8/+37
| | | | | | | | | | | | | | | | | lock around forward(), the first try queued before it runs - state.delivered[saleHash] is saved once a delivery succeeds; any later caller (retry tick, settle loop, /admin/forward) returns without paying - a sale already being delivered refuses a second concurrent caller (stays queued) instead of racing it - the settle loop queues a payment BEFORE its first try, so a restart mid-delivery no longer loses it (the retry finds it; the records keep it from paying twice) - the retry tick no longer overwrites the queue with its snapshot (it dropped payments queued while it ran) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* names: hold the second key of the double-payout claims (safeguard01 / ↵Adam Soltys4 days1-1/+2
| | | | | | | user42a0887a / sloperated) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* names: a Lightning delivery that timed out is never paid again while it's ↵Adam Soltys4 days1-0/+42
| | | | | | | | | | | | | | | | still in flight forward() records the payment hash of every Lightning delivery before it pays (state.inflight, keyed by the settled payment). A later try checks it first: complete = delivered, pending = wait, failed or never sent = go again. Before, waitsendpay's 240 s timeout queued the payment while its HTLC was still out, and the retry paid it a second time once the wallet claimed the first. A scripted wallet (usera7fc7e2c, "crit1 … double payout") held its claims to do exactly that on 2026-10-27; its key's forwards are parked for review (HOLD_KEYS). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* messages: two reactors, one per CPU the container getsAdam Soltys5 days1-0/+2
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* messages: messages.coinos.io, a Magnitude Relay for the app's private ↵Adam Soltys5 days5-1/+96
| | | | | | | messages, inbox lists, calls and group chats; copies down from relay.coinos.io over NIP-77 (the strfry tunnel), never writes into it Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* nwcpush: NWC wake pushes live 120 s at APNs, the request window, so a phone ↵Adam Soltys8 days1-1/+1
| | | | | | in deep sleep gets the wake late rather than never Co-Authored-By: Claude Fable 5.1 <[email protected]>
* vmedia: pic.webp?s=row, a picture 360px tall in its own shape for the grid's ↵Adam Soltys9 days1-3/+5
| | | | | | justified rows Co-Authored-By: Claude Opus 5.5 <[email protected]>
* feed: note and profile pages swap the site's share tags for their own, with ↵Adam Soltys9 days2-7/+13
| | | | | | the site card as the fallback picture (same as coinosv3 f3a3d88) Co-Authored-By: Claude Opus 5.5 <[email protected]>
* names: custom offers carry their owner's key; ones that can't reach a wallet ↵Adam Soltys9 days1-9/+32
| | | | | | | | | | | | | | | | are retired Releasing a name disabled its own offer but left its custom offers live (27 orphans on 2026-10-22, rick@ had 4): a payment to one found no name and no key, and would have sat in the forward queue for good. Custom offers now record the owner's pubkey (backfilled at start from the name, or for an orphaned npub1xxxxxxx@ name from the one current key with that npub prefix — 4 recovered), and the settle loop forwards by it, so a released name's offer pays the same key's name now, as the name's own offer already did. Orphans with no owner to follow are disabled on the node and dropped (23), as are a released staging name's. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* names: mint nothing payable for a wallet on another network, and retire what ↵Adam Soltys9 days1-2/+66
| | | | | | | | | | | | | | | | | | was minted A staging name is a mutinynet wallet, but every claim got a mainnet BOLT12 offer on our node published in its record, and custom offers and POS sales were minted there too: real sats paid that way had no route onward (2,092 sat parked on 2026-10-22). otherNetwork() — the record's ark address decides, a staging name without one is mutinynet — now gates the claim's stand-in offer, custom offers and POS invoices (LNURL already refused staging). At start, retireOtherNetworkOffers() disables the offers already out there (83 staging names, 13 custom offers), takes their lno= out of the published DNS record and drops them from state; idempotent, so a failure retries next start. [email protected] keeps its offers: its wallet is mainnet. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* names: a queued payment the float can cover goes now, backoff or notAdam Soltys9 days1-1/+5
| | | | | | | | | Float top-ups that missed their 60 s settle window landed later (+300k on 2026-10-22 once the ASP's vtxo pool refilled), but only a top-up seen to settle resets the queue's backoff, so jarryd's 248k sat in a 1-hour wait with 477k in the float. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* names: a wallet on another ark server gets its own Lightning invoice, one on ↵Adam Soltys9 days1-16/+49
| | | | | | | | | | | | | | | | | | | another network is parked The float step threw 'address belongs to a different ark server' before the Lightning step ever ran, so manna and rogerv3 (wallets on Second) were retried 100 times since 10-18 without once asking their wallets for an invoice. The destination's ark id now decides: the float only on our own server, otherwise straight to the wallet's CLINK invoice — those wait for the wallet without paging (waitingOnWallet in /health). A staging name's tark address paid real sats through its mainnet offer has no route at all: such a forward is parked (state.parked, with its reason) instead of retried forever, and /health counts it for the desk monitor. Ten such payments (2,092 sat, Sep–Oct test payments to @staging.coinos.io names) had been retried ~900 times each. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* names: deliver to coinos wallets over a circular route, shrink failing float ↵Adam Soltys9 days1-21/+96
| | | | | | | | | | | | | | | | | | | | | | | | | top-ups, report stuck money in /health The ASP's vtxo pool sat empty for 35 hours (2026-10-20/22: 614k in the rounds wallet against 1.12M needed to refill), and two forwards of ~124k for jarryd sat in the queue the whole time: - viaClink paid the wallet's own invoice with a plain pay, but a coinos wallet's invoice is minted by our ASP on this very node: a self-payment the hold plugin never sees ('Destination said it doesn't know invoice'). It now goes out and back in over a channel pair (payCircular, shared with the float top-up) and waits for the wallet to claim, so a payment is delivered whenever the wallet is open, float or no float. - a float top-up that doesn't settle halves the next ask (down to 20k), back to full size after one lands: 79 tries of 200k failed where the pool's 100k coins could have funded half of it. - the retry tick can't overlap itself any more (a circular delivery waits up to 4 min; an overlapping tick would forward the same payment twice). - /health reports pendingSat, oldestPendingAt, the top-up failure streak and last error, and payments for wallets on another ark server apart (undeliverable): the desk float monitor pages on these. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Bun 1.3.14 → 1.4.3, from ghcr.io/coinos/bun (oven/bun without the emSign ↵Adam Soltys10 days4-6/+6
| | | | | | | | | ECC Root CA - G3, CVE-2026-111943) names, nwcpush, blossom, the vmedia image (and live with it) and the hook's bun install. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* live: a streamer can delete their own recording (POST /live/<id>/delete, ↵Adam Soltys12 days2-9/+47
| | | | | | | NIP-98), ending the stream if it's on. Its files go, and stream.json stays as 'deleted' so the address answers 410 until the trim takes it. A stream someone reported is taken aside instead (.removed/, kept for reporting), so deleting can't destroy what was reported. Removed and deleted streams no longer count as live (toward MAX_STREAMS, or as exempt from the trim). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* live: WebRTC through Cloudflare's SFU (Realtime) for the app's viewers, a ↵Adam Soltys12 days2-2/+103
| | | | | | | | | | | fraction of a second behind the broadcaster where HLS is 3.5 s. The server only brokers it, and the app's secret stays here ($LIVE_CONFIG sfuAppId/sfuSecret): - POST /live/<id>/rtc/publish (the sender, NIP-98) pushes its tracks. - POST /live/<id>/rtc/watch hands a viewer an offer of them, and /rtc/answer takes its answer. /stats says which publish is current (rtc), and ?watching=1 counts a WebRTC viewer, who never fetches the playlist. HLS carries on as before, for the recording and the other nostr apps. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* live: 1 s HLS segments (keyframes every second), 10 in the live window — a ↵Adam Soltys12 days2-3/+5
| | | | | | | player starts about three segments behind the edge, so most of a viewer's delay was the 2 s segments (8.6 s behind the broadcaster, measured; 4.5 s with these). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* live: a name streams a day after it's claimed (the registrar now records ↵Adam Soltys12 days3-17/+151
| | | | | | | when; older records count from their last update), never if banned; GET /live/allowed says which and until when. Viewers can report a stream (POST /live/<id>/report, NIP-98): kept in .reports.jsonl and DM'd to the admins from the welcome key. An admin takes a stream down (POST /live/<id>/remove {ban}): it ends, its playlists and segments answer 410, its files move to .removed/ (kept for reporting, never trimmed), and a ban stops its sender streaming. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* live: recordings are kept 90 days, not 7 (the app lists past streams).Adam Soltys12 days1-1/+1
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* nwcpush: calls push only to an app that says it rings for them ↵Adam Soltys13 days2-3/+7
| | | | | | | (reasons.call), not to an older one whose worker would show a call as chat. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* nwcpush: calls wake a closed app. Kind 21059 wraps to a registered pubkey ↵Adam Soltys13 days2-4/+128
| | | | | | | are watched; one big enough to be a call offer (an SDP) pushes 'call' at high urgency, a minute's TTL, naming the wrap, which the device fetches from GET /wrap/<id> (kept two minutes; a push can't carry it) and opens with its own key. ICE candidates and hang-ups are too small to wake anyone. A 'call' opt-out like the other reasons. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* live: /live/turn — ICE servers for the app's calls (Cloudflare TURN ↵Adam Soltys13 days3-5/+50
| | | | | | | credentials good for 4 h, to a NIP-98-signed ask; 30 an hour per key, 60 per IP; STUN alone if TURN can't be had). The key comes from $STATE/live/config.json. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* live: /live/ping, a WebSocket that says pong — whether sockets get through ↵Adam Soltys13 days1-0/+4
| | | | | | | to the service. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* live: live streams sent from the app — the browser's recording over a ↵Adam Soltys13 days5-1/+415
| | | | | | | WebSocket (on 443, through Cloudflare: no ports to open), ffmpeg to 2 s HLS (H.264 720p, AAC), a live window and the whole stream (its recording), viewers counted, a dropped sender resuming the same stream, NIP-98 + coinos-name owners only, 3 at once, 4 h each, recordings kept a week. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* vmedia: /vmedia/hls.m3u8 — long clips as 360p/720p HLS made as they're ↵Adam Soltys13 days1-6/+193
| | | | | | | watched (mirrors coinosv3 b929771; the live container still runs ~/vmedia/server.js). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* vmedia: /vmedia/first.jpg — a clip's very first frame at full size, for ↵Adam Soltys2026-10-161-3/+29
| | | | | | the video pager's slides ahead (mirrors coinosv3 d718fce; the live container still runs ~/vmedia/server.js). Co-Authored-By: Claude Fable 5.1 <[email protected]>
* coinosv3 → 73bffd9: the services run the wallet's current Ark code; ↵Adam Soltys2026-10-165-165/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | names/vendor/ goes. The pin had stayed at 082fc2e (Sep 14) while the app's Ark manager moved on — renewals priced at a 300-sat base fee and re-priced on "insufficient refresh fee", expiry-aware coin selection, standing (scheduled) renewals merged by unlock hash, policy-aware freeing of in-flight coins — so the registrar's forwarder float was a month behind the wallets it forwards to. The manager's constructor, the methods the services call (balance, sync, refresh, send, address, createLnInvoice, driveLn, startBoard, pendingActions, htlcClaim, htlcCosignRefund, _key) and the storage contract (load → state or null, save ← state) are unchanged between the two commits, and nothing new is imported from a package the server lacks. names/translate.js imports translate-split.js from the submodule now, as feed/cache.js already did lang-guess.js: the vendored copies were byte-equal to what the pin now carries. The README's note about them goes with them. Resolve-checked every service entry (bun build --target=bun); the moved tests (registry-ownership, public-feed-cache) and the bun-test files (deepl-translate, names/store) pass. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
* README: rolling backAdam Soltys2026-10-161-0/+8
| | | | Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Lockfile from Bun 1.3.14; deploy into an ordinary cloneAdam Soltys2026-10-162-7/+10
| | | | | | | | The lockfile was written by a newer Bun than the pinned image can read. The deploy hook now keeps its checkout as a normal clone of the deploy repo, because submodule commands need a working tree of their own. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* README, example config and the deploy hookAdam Soltys2026-10-164-1/+208
| | | | Co-Authored-By: Claude Opus 5.5 <[email protected]>
* names: keep state in SQLiteAdam Soltys2026-10-163-20/+242
| | | | | | | | | | | | | | | | | | persist() rewrote the whole state file, close to 10 MB, on every settled payment and every queue tick, so one crash during a write could take every name with it. State now lives in SQLite (bun:sqlite, WAL, synchronous=FULL): one row per name, offer, invoice and so on, with the forwarder wallet's state kept as a single row. Each persist() is one transaction that writes only the rows that changed, so a crash leaves the previous state or the new one, never a torn file. The in-memory state object and every persist() call stay as they were. On the first start the JSON state file is imported, and a corrupt one stops startup. The file is left in place, so a rollback to the JSON build still finds its data. store.snapshot() makes a consistent copy while the service runs, for backups. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Make this the coinosv3-server repo: the services, with coinosv3 pinnedAdam Soltys2026-10-1616-155/+377
| | | | | | | | | | | | | | | | Carved out of coinosv3 (names, feed, nwcpush, blossom, vmedia, bridge and their tests, with history). The shared wallet code they import (src/ark, noffer, bolt12, popular-feed) now comes from a coinosv3 submodule pinned at 082fc2e, the commit the services have actually run on, so moving here changes no behaviour; bumping it is its own commit. The two text helpers translation uses arrived on the server later than that commit and are vendored under names/vendor at the versions deployed, until the pin moves past them. Root package.json pins the npm versions installed on the server. compose.yml runs names, nwcpush and blossom from this checkout (read-only) with configs and state under $STATE, outside the repo, and pins the Bun image. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* names: refuse a corrupt state file; flush before replacing itAdam Soltys2026-10-161-2/+9
| | | | | | | | | | | The loader treated any read or parse error as a fresh registrar and started with no names, then persisted that empty state over the real one. A crash can leave the state file truncated, because persist() renamed the new file into place without flushing it. Now a missing file is a fresh start, an unreadable one stops startup, and persist() fsyncs the file and its directory around the rename. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* bridge/ln: attach the socket error handler before connectingAdam Soltys2026-10-161-2/+6
| | | | | | | | createConnection on a stale socket path threw ENOENT before a listener was attached and killed the process. Create the Socket, attach .on('error'), then connect. This was patched on the server and never committed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* names: bring in the registrar as it ran on csAdam Soltys2026-10-161-8/+79
| | | | | | | | | | | | | | | | The deployed registrar carried changes made directly on the server between Oct 10 and Oct 14 that never reached the repo: - forward() delivers to the same key's current name when the paid name was released in the meantime (a v2 migration swaps its throwaway npub name for the real one seconds after a payment can settle) - "nowhere to send" throws, so the payout stays queued instead of dropping - the forwarder float merges small coins (consolidateFloat) before a send hits the input cap - floatUpkeep skips coins already past expiry, which the server's sweeper has taken and whose refresh never settles Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Feed: a picture from a host that won't hand over its bytes paints from a ↵Adam Soltys2026-10-112-14/+113
| | | | | | server-made copy. The app shrinks a post's photo to the column's size itself (an 1100px copy made once, kept on disk) — but only when the host allows CORS; one that doesn't (primal's blossom, nostrcheck, most personal sites) left the <img> painting the original, and a 9528×6328 camera file froze a wheel scroll for 400 ms on its texture upload. vmedia now has /vmedia/pic.webp?s=col|tile&u=<picture>: the picture at 1100px wide (or a 360px square of its middle for the grid), made with libvips (shrink-on-load, so the original is never decoded whole; turned the way its EXIF says; alpha kept; stripped) as WebP, cached on disk and at the CDN for everyone. The usual guards: https on public addresses only, each redirect's host checked, 60 MB cap, the bytes sniffed (a GIF is refused — its copy would be a still), a few jobs at a time, 600 new copies per address per hour, an 8 GB cache. The client asks the server for the column copy when the host is known to keep its bytes (or has just refused, or there's no worker to make one here), keeps it in the same cache as the copies it makes, and takes the server's tile for the grid the same way; a picture the server has no copy for paints as before. The container gets libvips-tools (Dockerfile). tools/pic-proxy-test.js (canned server answers). Server measured on cs: a 3072×4608 photo in 0.5 s the first time, a millisecond after; feed-stability's two pre-existing asserts still fail on 491904e. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Translation: into the phone's language, and DeepL with your own key. ↵Adam Soltys2026-10-103-3/+84
| | | | | | | isolabellart saw the Translate button but nothing became Italian: the target was the app's language, coinos has no Italian UI (so it ran in English), and the language guess didn't know Italian. Posts now translate into the reader's choice (Settings → Nostr → Translation, any of 31 languages), else the phone's language when the app has no UI for it, else the app's; the guess knows Italian, Dutch and Polish. And, as Jumble offers: a DeepL API key of your own, kept on the device and sent per request as x-deepl-key — the registrar passes it to DeepL (api-free for ":fx" keys; DeepL refuses browsers) and never stores or logs it; a refused key or used-up quota says so once. names: translateDeepl (prose only, links/tags kept, detected source back, 401/402/422 statuses), CORS allows the header. Deployed to cs names. tools/translate-italian-test.js, tools/deepl-translate.test.js. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Names: letters and numbers only, 2–24 — coinos v2's rule. The registrar ↵Adam Soltys2026-10-091-2/+11
| | | | | | | took a-z, 0-9 and . _ - (1–30), so bitcoin-education and a dozen others got through. A NEW claim must now match /^[a-z0-9]{2,24}$/ (v2's /^[\p{L}\p{N}]{2,24}$/u, held to ASCII since a name is a DNS label); a name already held (19 with . _ -, plus "f" and "g") is grandfathered — its owner can still update it — as are v2 migrations carrying their own name and npub-shaped defaults. /name/<n> answers invalid: true for a free name that breaks the rule; the claim form says "Letters and numbers only" as it's typed, and the suggested name from a nostr profile is stripped to letters and digits. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Video stills: past a black opening. vmedia took its still at half a second, ↵Adam Soltys2026-10-091-7/+29
| | | | | | | and a clip that fades in from black (7 of the 68 made in three hours) gave a black tile. Each frame's mean brightness now comes out of ffmpeg (signalstats YAVG on pipe:2 — /dev/stderr can't be opened from a Bun-spawned pipe), and a dark one sends it further in (2, 5, 10, 20, 40 s) for the first bright frame; a clip dark all the way through (a night scene, a boot screen) gets its brightest frame with the levels stretched (normalize). Stills are made again under a new key and URL (v=4), so neither the server's nor the CDN's black copies are served. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Videos grid: a tile without a still of its own shows the server's (vmedia ↵Adam Soltys2026-10-091-1/+1
| | | | | | | thumb.jpg) at once — about a second the first time anyone asks for that clip, a CDN hit after — instead of waiting on the browser's probe (64 KB) and frame grab (≈1 MB of the clip, decoded) per tile, which on a phone left most of the grid as empty boxes. The frame grab still runs for a clip the server had no picture for. A known-heavy clip wears its size badge over the still. vmedia makes 8 stills at once (was 4). tools/video-tile-poster-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Translation: a hashtag in a script the reader can't read gets its ↵Adam Soltys2026-10-092-7/+10
| | | | | | | translation in brackets after it — "#総合部絵チャ (…)" — the tag itself kept as the link it is. A post that is only such hashtags now counts as foreign (it was judged by its prose alone, of which it has none, so nothing ever offered to translate it), and ⋯ → Translate on a post with no words says so instead of "already in your language". The registrar's language detection reads the glossed tags too; lang-guess's scriptOf takes a minimum (a two-character tag has a script too). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Translation: posts in another language are shown in yours — "Translated ↵Adam Soltys2026-10-094-0/+241
| | | | | | | from Spanish · Show original" under the words, automatic by default for posts the language guess calls foreign (Settings → Nostr → Translation switches it off and leaves a Translate link instead), and ⋯ → Translate for any post. The browser's on-device Translator does it when the language pair is already installed (desktop Chrome); otherwise the registrar's new POST /translate (names/translate.js): a cached, 4-at-a-time front for a LibreTranslate container on cs (names/compose-libretranslate.yml, v1.9.6, 32 languages; none for sw/mr/ta/te, so those readers get no offer). Links, nostr: mentions, :emoji:, #tags and long codes are never sent to the model — only the prose between them, batched in one call (src/translate-split.js, shared). Translations are remembered (coinos-translations, 300). tools/translate-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* POS terminals ring up in the owner's currency: the app sends its currency ↵Adam Soltys2026-10-081-1/+6
| | | | | | when minting a terminal token (it sent none, so every terminal priced sales in USD and a $0.03 sale paid out as $0.04 CAD), and a new token retunes the terminals already paired to that name Co-Authored-By: Claude Opus 5.5 <[email protected]>