The screen presented evidence and left the user to be the analyst; now it
draws the conclusions and links each one to the setting that acts on it:
- Insights: a UsageInsights rules layer (unit-tested, thresholds informed by
the ping study and normalized per observed day) renders up to three
plain-language recommendations, each deep-linking to notification
settings, media settings, the relay list, or privacy options.
- Rates: battery %/hour in app (from the measured drain sampler), data/hour
in app, and average simultaneous relay connections — totals aren't
judgeable, rates are, and avg relays is directly actionable.
- Subsystem bars rank by CELLULAR bytes when any exist (with totals as
secondary context) — Wi-Fi bytes are nearly free, so ranking by totals
pointed users at the wrong feature.
- Built-in Tor gets the same cost card as the always-on service: uptime,
bootstraps, and a link to privacy options.
- Per-screen time (screen.<Name>.ms): a ScreenTimeIntegrator fed by the nav
controller's destination listener, gated on foreground. PRIVACY: only the
route's base name is recorded — screenNameOf strips every navigation
argument before the value leaves the navigation layer, so the ledger says
'Profile', never whose profile. Shown as proportion bars and included in
the report's summary table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
The uptime row told users the service ran, not what it cost — and the
setting to turn it off lives three screens away. The resource screen now
shows a dedicated card (only while the service has run this week) that
frames the cost in decision terms: running time and restarts, the relay
connections it held while the app was closed, and the measured background
battery drain — with a button straight to notification settings where the
delivery method can be changed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
Uptime alone can't distinguish a stable 24h service from one the watchdog
restarts every hour — each restart is a process wake plus a full round of
relay reconnects. Reported alongside uptime in the usage report.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
- ScheduledPostWorkGate now treats PUBLISHING as pending work: claiming the
last PENDING post no longer cancels the periodic worker mid-publish (which
stranded the post in PUBLISHING with no recovery). ScheduledPostStore also
releases stale PUBLISHING claims at initial load — a claim found on disk
belongs to a dead process and is reset to PENDING for retry.
- CalendarReminderWorker decides its self-cancel on a FRESH cache snapshot,
closing the race where an RSVP accepted mid-run was lost forever (the
observer's KEEP schedule no-ops while the chain exists). A second observer
on kind-31922/31923 re-arms the chain when the organizer reschedules the
target event — the existing RSVP never re-fires observeNewEvents, so the
moved appointment previously produced no reminder. The rescan is throttled
(conflate + 30s) and gated on the feature being enabled.
- UsageCountingInterceptor skips loopback hosts: local Blossom cache hits
(127.0.0.1) never touch the radio and were inflating mobile rx bytes,
radio-burst estimates, and potentially the background-data alert.
- The usage alert's 7-day rate limit is now consumed when the user acts on
the dialog (any button or dismissal) instead of before the first frame, so
a config change or process death no longer burns the prompt budget on a
dialog nobody saw.
- DEV_REPORT_PUBKEY is defined once in service/crashreports and imported by
both the crash and resource-usage send paths.
- ResourceUsageScreen remembers its summaries keyed on the loaded data — the
2s memory ticker was recomputing the full multi-day counter walk on every
recomposition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
New counters for the app's remaining heavy battery consumers, all
transition-based so tracking never adds work to the activities measured:
- pow.ms/sessions: NIP-13 mining time from the PoW queue's isMining flow —
the largest attributable CPU burner, previously an unattributed cpu.ms
mystery
- tor.ms/starts: in-app Arti uptime from the raw TorService status (not
TorManager.status, whose WhileSubscribed upstream would keep Tor's
control flow alive if the ledger subscribed to it)
- service.alwayson.ms, call.ms/sessions, nests.ms/sessions: foreground
service lifecycles for the always-on relay service, calls, and NIP-53
audio rooms
- crypto.decrypt/encrypt.count+us and sign.local/nip46/nip55.count via a
MeteringNostrSigner decorator wrapped inside NostrSignerWithClientTag at
account load; crypto durations only metered for local-key signers so
IPC/relay waits never poison the CPU numbers
- location.ms: GPS listening segments around LocationFlow collection
- battery.drain.fg/bg: measured percent-while-discharging sampled at each
flush — the ground truth to correlate the other counters against
Ledger infrastructure hardening the new counters build on:
- all duration math now uses SystemClock.elapsedRealtime; wall-clock jumps
(NTP, timezone) no longer fabricate or delete accounted time
- pre-flush-hook adds are drained in place and no longer re-arm the
debounced flush, killing a self-perpetuating 30s wake-and-write loop
- drain is now AtomicLong.getAndSet(0) instead of remove+sum, closing a
lost-update race that undercounted the hottest counters
- shared TimeSegmentIntegrator base extracted; relay/foreground integrators
and the new SessionTimeIntegrator all use it
- @Volatile on LocalCache.verifyMeter (matching the onchainBackend
precedent)
New counters surface in the usage screen (zero rows hidden), the NIP-17
report tables, and the per-day dump. 9 new unit tests including a
regression test for the flush loop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
Closes the gaps the "should we track the non-websocket client?" question
exposed, and adds the battery-estimation layer bytes alone can't provide:
- Counting moves from per-role wrappers to a single interceptor on the
shared base client (OkHttpClientFactory), with role attribution via
request tags (role wrappers now only tag, inserted outside the counter).
Anything reaching the shared clients untagged lands in a new "other"
subsystem instead of escaping the ledger — which the napplet broker did,
and which Coil's image traffic did too: setImageLoader called
getHttpClient directly, bypassing the metered okHttpClientForImage.
The app's largest HTTP consumer was invisible; now it's tagged "image".
- Battery-relevant shape of the traffic, not just its size: per-subsystem
request counts and active-transfer time, plus a radio-burst estimator
(new HTTP activity after >10s of silence ~= one cellular radio
ramp+tail). Scattered small requests cost far more energy than one
continuous download of the same byte count; bursts/day makes that
pattern visible and reportable.
- Media playback time (ExoPlayer isPlaying segments via a listener in
ExoPlayerBuilder — every player in the app): decoder+screen+streaming
at once, turning "video used 800 MB" into "800 MB over 2h" (normal)
vs "over 10 min" (a bug).
All surfaced in the usage screen, the NIP-17 report, and UsageSummary;
20 ledger tests total (burst gap logic, play-time segments, and the
existing suites) all passing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
The first version was a wall of identical label-value rows: correct but
unreadable at a glance. The redesign gives the screen visual hierarchy
and proportion:
- Headline 2x2 stat tiles for today (cellular, wi-fi, relay time, time
in app) — the numbers that matter get the visual weight.
- A 7-day data-per-day trend chart (Vico, same line/area idioms and
weekday axis as the notification summary chart) with a cellular/wi-fi
legend. Color pairs validated for CVD separation and lightness on both
surfaces: BitcoinOrange+RoyalBlue on light, #C77414+RoyalBlue on dark
(a selected dark variant, not an automatic flip).
- Data-by-feature as ranked proportion bars, so "video dwarfs relays"
is visible without reading numbers.
- Live memory as meters: the heap bar keeps the retired debug chip's
green/amber/red thresholds; image caches as fill bars.
- Activity counters stay as compact rows under one section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
Moves the debug-build memory chip (home top bar) into the resource-usage
screen as a "Memory right now" card visible to everyone: app heap, native
heap, Coil RAM/disk cache fill, LocalCache note/profile/addressable/
chatroom counts, and the device memory class — refreshed every 2s only
while the screen is composed (same off-main collection the chip used,
DiskLruCache.size() contends with journal I/O).
The same snapshot is appended to the NIP-17 usage report (both from the
screen's send button and the high-consumption dialog), so a report now
also shows whether the device was under memory pressure at send time.
MemoryUsageChip.kt is deleted and the chip call removed from
UserDrawerSearchTopBar — the debug feature is retired from the home UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
Extends the resource-usage ledger with the metrics that answer "what else
could be draining":
- Relay (re)connection + failed-dial counters per network/visibility via
the existing RelayConnectionListener hook. Every completed connect paid
a TCP+TLS handshake, so high daily counts are the reconnect-churn
signature — new alert threshold at >5000 connects/day.
- Whole-process CPU time (Process.getElapsedCpuTime deltas sampled at
ledger flush): the honest aggregate of parsing, crypto, coroutines and
UI — one syscall per flush, nothing while idle. Answers whether CPU
matters at all before any per-subsystem drill-down.
- Foreground time (time with UI visible): what display power scales with,
and the denominator that makes every other per-day number interpretable.
- Signature-verification count + CPU time via a nullable meter hook on
LocalCache.justVerify (wired like cache.onchainBackend), settling the
"does Schnorr verify cost matter" question with data.
Deliberately not tracked (documented in the plan): per-screen time (route
names leak behavior patterns into a shareable report — needs its own
privacy pass), per-coroutine CPU (needs a thread registry; cpu.ms first),
and signing (user-action-rate, negligible).
All metrics surface in the usage screen, the NIP-17 report, and
UsageSummary; 4 new tests (18 total for the ledger).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
Passive, always-on counters (never transmitted automatically) that make
battery/data complaints diagnosable in production, where they actually
happen — see amethyst/plans/2026-07-12-resource-usage-ledger.md:
- Per-subsystem HTTP bytes (image/video/uploads/money/nip05/preview/push)
via a counting interceptor wrapped per role in RoleBasedHttpClientBuilder,
split by cellular/wifi and foreground/background.
- Relay traffic (RelayConnectionListener) and relay connection-time
(Σ connections x time, integrated from connectedRelaysFlow with no
timers) — the ping study's best single battery proxy.
- Notification wakelock held-time, background worker runs, and app
process starts (WorkManager churn detector).
- Daily buckets persisted to one small JSON file (ScheduledPostStore
idiom), pruned after 30 days; counters are sizes/durations/counts only,
no URLs, relay names, or content.
Surfaces:
- Settings > "App resource usage": today / 7-day summaries and a
data-by-feature breakdown, plus an explicit "send report via DM"
button that prefills the NIP-17 composer to the developers (same
pipeline, recipient, and 30-day expiration as crash reports — nothing
sends until the user taps Send).
- High-consumption prompt: on app open, if the last day crossed
conservative thresholds (>50 MB background cellular, >12 relay-hours
backgrounded on cellular, >30 min notification wakelock, >75 process
starts), an AlertDialog offers review-and-send — at most once every
7 days, with a persistent "don't ask again".
14 unit tests cover the store, accountant (day rollover, live-merge,
pre-flush hooks), connection-time integrator (incl. the long-stable-
session case), and alert thresholds/rate limiting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
The ping-interval change shipped on reasoning and was overturned by
measurement; these tests give the remaining battery changes the same
scrutiny where the claims are about OUR code (deterministically
testable), instead of leaving them reasoning-only:
- NostrClientKeepAliveTest (quartz): a server-closed relay is redialed
within one 60s sweep while active; ZERO dials happen while the client
is inactive no matter how much time passes; and the sweep provably
resumes after connect() — the failure mode a suspend/resume bug in
the new isActiveFlow gate would cause.
- ScheduledPostWorkGate extracted from AppModules + tests: schedules on
the first PENDING post, cancels when the last drains, re-schedules on
publishNow-retry, and — the race that motivated the extraction —
never emits a spurious cancel from the store flow's empty placeholder
before the disk load completes.
- CalendarReminderWorker.couldStillFire extracted + tests against
LocalCache: future target keeps the chain, past target lets it end,
and an unresolved/start-less target keeps it alive so a reminder
can't be lost while the target event is still being fetched.
The watchdog alarm change (wakeup -> non-wakeup) remains reasoning-only
by nature: it asserts Android OS alarm semantics on OEM devices, which
no JVM test can exercise; its blast radius is limited to opt-in
always-on users with two independent restart layers still active.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
Probed 122 production relays (top of 1,468 harvested from NIP-65 lists)
with idle connections and client pings at 55/110/120/180/240/300s.
Findings, detailed in amethyst/plans/2026-07-12-relay-ping-interval-study.md:
- 99/122 relays survive 13 min of total client-ping silence; 90 of those
send their OWN pings every 30-70s, which OkHttp must answer — so the
cellular radio's wake cadence is set by the relays, not by our ping
interval. The battery saving of the 240s mobile interval was illusory.
- Idle-timeout tiers exist at ~60/~120/~240/~300/~600s, and pings only
reliably reset a relay's timer when the interval is well below the
tier: 240s pings drop relay.ditto.pub and the nostr1.com hosting tier;
300s pings drop even relay.snort.social. 120s holds every tier >=240s.
- Lowering below 120s would only rescue a ~120s tier (6 relays) that
already cycles today under 120s pings (110s/120s both fail; 55s works).
Net: 120s is the measured sweet spot; the mobile/wifi split is removed
and the constant now documents why.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
Five targeted fixes for the biggest idle drains:
- NostrClient keep-alive sweep: the 60s reconnect loop kept ticking for
the whole life of the process even after disconnect() (i.e. the entire
time the app sat in the background). isActive is now a StateFlow and
the loop suspends on it, so an inactive client schedules zero timers.
- Relay WebSocket pings on cellular: every ping on an idle connection
promotes the radio out of its low-power state, per connection. The
mobile-data client now pings every 240s instead of 120s (wifi keeps
120s), staying under common carrier NAT idle timeouts.
- Always-on service watchdog: the 5-min health-check alarm no longer
uses the _WAKEUP variant. Pulling the CPU out of sleep to restart a
service that can't do useful network work on a sleeping device was
pure cost; the alarm now fires as soon as the device is next awake,
and the WorkManager + FCM/UnifiedPush layers still cover doze.
- ScheduledPostWorker: the 15-min periodic worker was enqueued forever
for every user, waking (often cold-starting) the process with nothing
to publish. It is now enqueued exactly while the store holds a PENDING
post, driven by a store-flow observer in AppModules.
- CalendarReminderWorker: same unconditional 15-min periodic schedule,
but worse — LocalCache is memory-only, so a WorkManager wake of a dead
process can never find an RSVP to remind about. The worker is now
scheduled when an ACCEPTED RSVP lands in LocalCache and cancels its
own chain when nothing upcoming remains (or the feature is disabled).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
- Apply Kover (Apache-2.0, build-time only) to the KMP/JVM modules and aggregate at the root
- include amethyst playDebug in the aggregated Kover coverage
Clients in the wild publish structurally wrong values in profile
metadata — e.g. "nip05":{} — and the strict field serializers made
JsonMapper throw, so contactMetaData() returned null and the entire
profile (name, picture, about…) was discarded.
Generalize the BirthdayTolerantSerializer precedent: string fields now
use TolerantStringSerializer (accepts any JSON primitive, ignores
objects/arrays/null) and the bot flag uses TolerantBooleanSerializer,
so a single malformed field is skipped instead of being fatal.
Non-JSON content (e.g. "Relay initialized") remains unrecoverable and
still parses to null; a test pins that it does so without throwing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSCGp6KgcnaepEMbxNroYe
The READ_OUTBOX relay-auth prompt and the settings toggle described the
permission as only loading posts, but authenticating to a follow's relay
also downloads their profile information and post engagement. Update the
prompt title, consequence line, reason label and toggle description to
say so.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VmzSAtkFv29KWDzPVBbxS
Each PoW job now races PoWPolicy.minerWorkers(cores) = cores/2 parallel
searches (at least 1) instead of a single thread, so a post on an 8-core
phone mines ~4x faster while the other half of the cores stays free for
the UI, and the total CPU budget matches the old 2-job x 1-thread pool.
- PoWPolicy.minerWorkers(availableProcessors) is the single definition of
the per-job worker budget.
- PoWPublishQueue gains minerThreads (used by its built-in miner and
exposed for custom mine lambdas); AppModules wires maxConcurrent = 1 +
minerThreads = half the cores.
- PoWNostrSigner gains a workers param (default 1), covering reactions,
reposts and reports via Account.miningSigner; the anonymous-post paths
in the short-note and comment composers pass the same budget.
- Gift-wrap envelope mining stays single-threaded: the template
conversion is a non-suspend hook inside the synchronous NIP-59 build.
- UI time estimates (settings picker, composer chip, broadcast banner)
now benchmark at the queue's worker count via deviceHashesPerSecond(),
and PoWEstimator caches one rate per worker count.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhaF5scnAvhP9wr9R7bTWM
The miner enumerates the nonce space deterministically (the random base is
overwritten before the first hash), so naively racing N copies of the search
duplicates the exact same candidate sequence N times. PoWMiner.mine() now
races workers over disjoint slices instead: each worker's nonce carries a
distinct fixed prefix while only the bytes after it are enumerated, making the
aggregate hash rate scale with cores (~3.8x on a 4-core box).
The hot loop also switches from sha256() to sha256Into() with a reused
32-byte buffer, so hashing no longer allocates per attempt.
amy wiring:
- `pow mine` and `post --pow` mine on all cores by default; `pow mine
--threads N` overrides.
- `pow bench` measures the all-cores rate (what mining now uses, also the
basis for expected_seconds) alongside a new hashes_per_second_single_core.
- PoWEstimator benchmarks with sha256Into to match the miner, and gains a
workers overload that prices in cross-core contention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhaF5scnAvhP9wr9R7bTWM
The PoW publishing-queue branch split broadcastPrivately into a
NIP17Factory.Result overload delegating to a List<GiftWrapEvent> one,
while the DM-unread branch appended markDmRoomAsRead(signedEvents.msg)
to the old single body. The merge left that call inside the wraps
overload, where signedEvents does not exist, breaking compilation.
Move the read-marker call to the Result overload, and mark the room as
read on the PoW early-return path too (the rumor is signed inline there
and the wraps only publish after mining), so PoW-enabled accounts keep
the unread-clearing behavior from #1286/#1287.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1hu9zpyiuJo6yyoJs1vdw
The mining banner's indeterminate bar becomes determinate: each mining job
gets its own bar that fills over the device's estimated duration for its
difficulty (elapsed / (2^bits / benchmarked hash rate)), with an
"≈ 10 minutes left" label next to the elapsed time. The nonce search is
memoryless, so past the mean the bar honestly falls back to the
indeterminate sweep with "any moment now" instead of parking at 100%.
Queued-only banners keep the shared activity sweep.
The mining notification mirrors this: single-job progress fills toward the
estimate with the time-left text, refreshed every 30 s while mining (the
queue only emits on state changes, not clock ticks).
The composer difficulty menu now prices each option on this device —
"24 bits · ≈ 45 seconds" — using the same cached benchmark, and the duration
formatting is shared (formatApproxDuration/formatTimeLeft) with the settings
estimate instead of being private to the settings screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
The per-post PoW button on the New Post screen now sits before the
Subject/Title toggle instead of last in the options row.
Every PoW surface (composer button, note chip, mining banner, settings tile,
relay-info minimum-PoW row) switches from the Bolt icon to Manufacturing
(gear, U+E726): Bolt is the app's zap/lightning glyph everywhere else, so a
Bolt PoW badge next to the zap-split/zapraiser/invoice buttons read as a
payment feature. Subset font regenerated for the new codepoint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Shows a button in the relay information header that opens the relay's
NIP-29 group list (RelayGroupServer route) when the relay's NIP-11
document advertises support for NIP-29. Also converts the header button
row to a FlowRow and marks button labels single-line so labels no
longer break mid-word (e.g. "Member/s") when the row runs out of
width.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012w9KridAbRpesbX2ytdjn4
GiftWrapEvent.create no longer takes powDifficulty/powIsActive — it takes a
single templateConversion hook ((template, ephemeralPubKey) -> template,
default identity) that runs on the finished wrap template right before the
ephemeral key signs it. The hook receives the ephemeral pubkey because the
NIP-01 id a nonce commits to includes it and the key never leaves create().
NIP17Factory forwards the same hook through wrapSeal/createWraps and the
create*NIP17 entry points, so quartz's NIP-59/NIP-17 code no longer imports
the NIP-13 miner at all; Account builds the mining closure at the call site.
Any future pre-sign wrap adjustment flows through the same seam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Durability & correctness:
- Keep the job entry + disk checkpoint alive until the publish continuation
completes (was: dropped at mining-complete); failed publishes keep their
checkpoint for restart retry and surface through a failures SharedFlow
- Move draft deletion into the publish continuations in every composer so a
cancelled or process-killed mining job can't destroy the only copy of a post
- Persist gift-wrap mining: split NIP17Factory into createSeals (signer
interaction, runs inline) + wrapSeal (pure CPU, runs on the queue), new
REPLAY_WRAPS records restore pending DM wraps after process death
- Clamp synced NIP-78 difficulty (PoWPolicy.MAX_DIFFICULTY), require a sane
target in PoWMiner, bound PoWRankEvaluator against short ids
- Reaction double-tap while mining now toggles (dedupeKey + cancelByKey)
instead of publishing duplicates
- Restore checkpoints for every loaded account, not just the active one
- logOff purges the account's checkpoints and cancels its queued jobs
- Private notes: composer chip now gates on the gift-wrap kind and the
per-post override reaches sendPrivateNote
Coverage:
- Public/live chat (kinds 42 + 1311) and voice replies (1244 + kind-1 audio
replies) now route through the mining gate
Perf:
- FGS start() dedupes with a running flag; PendingIntents built once
- Banner 1 Hz clock only ticks while a job shows elapsed time
- PoWEstimator benchmark is single-flight behind a Mutex
UX / l10n:
- Post-mining failures toast with retry information
- Count strings converted to <plurals>; elapsed time via DateUtils; settings
estimate uses localized units; shared powKindLabelRes replaces three
duplicated kind→label maps; dead pow_chip_* strings removed
- CLI: pow mine lowercases the pubkey before mining (uppercase hex mined an
id that never matches the signed event) and validates via quartz Hex
New queue tests: checkpoint lifetime, failure reporting, cancelByKey toggle,
per-owner cancellation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V