The `sinceLastTag.updated` field was set to `date -u` on every
`scripts/translators.sh --seed` run, but nothing ever reads it. The
release-time credit generator only consumes `.mappings` and
`.sinceLastTag.translators`.
Because the field changed on every run, the seed-translators CI job
produced a diff (and therefore a new Crowdin/seed PR) on every push to
main even when the translator set was unchanged. Drop the field from the
seed write and from the committed JSON so the file only changes when a
contributor actually appears.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSoN4DDC5F1ehwGeC33652
Replace startsWith(..., ignoreCase = true) — which case-folds on every
call — with the precomputed DualCase prefixes and the new
String.startsWith(DualCase) helper, so the cashuA/cashuB dispatch only
compares against already-cased strings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
v1.12.4 got the keychain right ("...in keychain [/Users/.../amethyst-signing
.keychain-db]") but createReleaseDistributable still failed with "Could not
find certificate". Different layer of the same problem:
Compose's MacSigner maps its identity to a cert by running `security
find-certificate -c <identity>`, prepending "Developer ID Application: " when
the identity doesn't already start with it. `codesign --sign` (used by the
signMacJarNatives task, which succeeds in the same job) instead matches a
SHA-1 hash OR any common-name substring. So a MAC_SIGN_IDENTITY secret that is
a fingerprint or a team-ID/partial name signs fine with codesign but, once
prefixed by Compose, is not a substring of the cert's common name -> zero
matches -> failure.
Reproduced locally against the real Developer ID cert:
find-certificate -c "Developer ID Application: <TEAMID>" -> 0 matches
find-certificate -c "Developer ID Application: <full CN>" -> 1 match
Fix: import-macos-cert now resolves the certificate's full "Developer ID
Application: NAME (TEAMID)" common name from the keychain (via find-identity)
and exposes it as an `identity` output. The desktop build feeds that to
Compose's signing.identity, falling back to the raw secret if resolution
fails. Independent of whatever form the secret takes. The amy CLI leg keeps
using bare codesign with the secret directly and is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The desktop DMG release leg (build-desktop macos, packageReleaseDmg) has never
produced a signed artifact: createReleaseDistributable fails with "Could not
find certificate for '***' in keychain []". This is independent of the v1.12.3
notarization fix, which addressed the separate amy CLI leg.
Root cause: Compose's MacSignerImpl maps the signing identity to a certificate
by running `security find-certificate -a -c <identity>` with no keychain
argument. On the GitHub macOS runners that lookup does not resolve the cert that
import-macos-cert imported into a throwaway keychain and added only to the user
search list — even though bare `codesign --sign` (e.g. the signMacJarNatives
task, which succeeds in the same job) finds it fine. The "keychain []" in the
error is just the null settings.keychain being echoed.
Fix: export the throwaway keychain path from the import-macos-cert action and
feed it to Compose's `signing.keychain` via AMETHYST_MAC_SIGN_KEYCHAIN, so the
certificate lookup searches that keychain directly. Also set it as the default
keychain for good measure. No-op on local/PR builds (env unset -> Compose keeps
its previous default-search-list behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes docs/changelog/translators.json self-sufficient so a release no longer
needs to re-query Crowdin:
- sinceLastTag entries now carry each translator's languages ({user, languages}),
recorded by the --seed run.
- Default mode (no flags) is offline: it generates the "## Translations" block
straight from the committed file — reading sinceLastTag, grouping by the stored
languages, and resolving npubs via the mappings registry. No token, no network.
- --seed/--raw remain the online paths (CI seeding / debugging). curl + git +
credentials are only required there; the offline path needs just jq.
RELEASE_OPS now points at the tokenless `scripts/translators.sh` for the
changelog credits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWQWVdWLAwBUBX2gJ55y6b
- parseCashuA now decodes standard *and* url-safe base64. NUT-00 v3
specifies base64-urlsafe, but legacy encoders (and older Amethyst
builds) emitted standard base64; try standard first, fall back to
url-safe so both round-trip.
- CashuWalletViewModel.redeemToken now redeems every mint/keyset group
in a pasted token instead of only the first, validating all mints are
in the wallet up front and summing the redeemed amounts.
Adds parser coverage for both base64 alphabets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
Reworks docs/changelog/translators.json into two lists maintained by
scripts/translators.sh:
- mappings: a forever-growing Crowdin-username/id -> npub registry. --seed
appends new contributors with a blank npub and never deletes or overwrites
existing entries.
- sinceLastTag: a rolling snapshot of who has translated since the last v* tag,
fully refreshed on every --seed run.
The contribution window now defaults to the most recent v* tag instead of a
fixed two months (falling back to two months ago when no tag is reachable). The
CI seed job fetches tags (fetch-depth: 0) so it can resolve that window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWQWVdWLAwBUBX2gJ55y6b
Audit follow-ups on the cashu token codec move:
- CashuTokenB64Parser.parse() dispatches on the prefix case-insensitively
(matching commons RichTextParser's case-insensitive cashuA/cashuB
detection), but parseCashuA/parseCashuB stripped it with a case-sensitive
removePrefix — so a mixed-case prefix passed dispatch and then fed its
own prefix bytes into the Base64 decoder, failing to parse. Strip the
fixed 6-char prefix with drop() so dispatch and stripping agree.
- hoist a single shared CashuV4Cbor instance instead of allocating a new
Cbor on every encode (V4Encoder) and every cashuB parse.
Adds an acceptsMixedCasePrefix regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
Adds a seed-translators job to the Crowdin workflow that runs
scripts/translators.sh --seed (past two months) and opens/updates a single
PR via peter-evans/create-pull-request whenever a new contributor appears, so
docs/changelog/translators.json stays current without manual upkeep. The
action is MIT and CI-only (not linked into any shipped artifact), and no-ops
when there is no diff.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWQWVdWLAwBUBX2gJ55y6b
scripts/translators.sh --seed fetches every contributor in the window
(default: past two months) and merges their Crowdin usernames into
docs/changelog/translators.json with blank npubs, preserving existing
entries and deduping case-insensitively. Fill in the npubs afterwards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWQWVdWLAwBUBX2gJ55y6b
Moves the Crowdin translator-credits generator from tools/translators/ to
scripts/translators.sh to sit with the other flat shell scripts. Drops the
standalone README (the script is self-documenting via --help) and folds the
release-time usage into RELEASE_OPS.md next to the changelog step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWQWVdWLAwBUBX2gJ55y6b
Adds tools/translators/translators.sh, which pulls a Crowdin "Top Members"
report for a release window (between two tags/dates) and prints the changelog
"## Translations" block grouped by language.
Crowdin contributors are joined against docs/changelog/translators.json, a
Crowdin-username/id -> npub mapping kept alongside the changelogs. Contributors
with no mapping are listed under UNMAPPED so they can be credited by hand and
backfilled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWQWVdWLAwBUBX2gJ55y6b
Follow-up to the V4Encoder move. Quartz could encode a cashuB string but
could not parse cashuA/cashuB back, and the parsing lived in amethyst even
though it is pure NUT-00 wire-format protocol. Worse, commons.RichTextParser
already detects cashuA/cashuB words while the parser sat up in the app, so
Desktop (its own rich-text viewer) could not parse a received token at all.
Consolidate the legacy out-of-band redeem stack onto quartz:
- new quartz CashuTokenB64Parser parses cashuA (standard-Base64 JSON, rewritten
off Jackson onto kotlinx.serialization to satisfy quartz's no-Jackson rule)
and cashuB (Base64URL CBOR, reusing the V4Token models), returning quartz
types. It is the inverse of V4Encoder.
- move the CashuToken container model from commons to quartz, switching its
proofs from the duplicate commons Proof (field C, amount Int) onto the
canonical quartz CashuProof (field c, amount Long). The duplicate Proof
type is deleted.
- delete amethyst V3Parser/V3Token/V4Parser; CashuParser/CachedCashuParser
stay as thin amethyst adapters (off-main-thread guard + GenericLoadable +
LruCache) over the quartz parser.
- this removes the manual Proof -> CashuProof conversion shims that
MeltProcessor and CashuWalletViewModel previously carried.
Tests: full cashuA + cashuB vector coverage moves to quartz commonTest
(CashuTokenB64ParserTest, runs on JVM) plus an encode/parse round-trip; the
superseded amethyst CashuV4ParserTest is removed and CashuBTest stays as the
adapter integration test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
The NUT-00 v4 `cashuB` wire-format encoder + CBOR models lived in
`amethyst/service/cashu/v4`, even though they are pure protocol code with
no app/UI dependencies. quartz had the proof/mint primitives but no
`cashuB` string codec at all, so this was a layering gap rather than a
duplicate.
Move `V4Encoder` and the `V4Token`/`V4T`/`V4Proof`/`V4DleqProof` wire
models into `quartz/nip60Cashu/token`, rewriting the JVM-only
`java.util.Base64` to the multiplatform `kotlin.io.encoding.Base64.UrlSafe`
so the codec lives in commonMain. This unblocks sharing CashuWalletOps /
CashuWalletState into commons (V4Encoder was one of the Android-only ties).
- add kotlinx-serialization-cbor (Apache-2.0) to quartz commonMain
- repoint CashuWalletOps (encoder) and V4Parser (V4Token) imports to quartz
- add V4EncoderTest round-trip coverage in quartz commonTest
amethyst's V4Parser stays put: it returns UI types (GenericLoadable +
commons CashuToken) and now just reuses the relocated quartz V4Token.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
The v1.12.2 release was the first to actually codesign + notarize the macOS
artifacts (signing was wired after v1.12.1, which shipped unsigned). Both macOS
legs failed with "Notarization status: Invalid": Apple's notary service recurses
into the bundled jars and rejects the unsigned Mach-O natives inside them
(secp256k1, sqlite-bundled, jna, skiko, jkeychain, kdroidFilter mediaplayer) —
codesign on the .app and the CLI's loose-file loop never descend into jars.
Notary log confirmed the offending entries, e.g.
sqlite-bundled-jvm.jar/natives/osx_arm64/libsqliteJni.dylib
-> "not signed with a valid Developer ID certificate" / "no secure timestamp"
Add scripts/sign-macos-jar-natives.sh: a shared helper that signs every macOS
Mach-O inside the bundled jars with hardened runtime + a secure timestamp,
skipping Linux ELF via a `file` Mach-O gate and no-opping when no identity is
set (local/PR builds unchanged). Wire it into:
- the CLI notarize step (runs before the loose-file signing loop)
- a desktop signMacJarNatives Gradle task that signs the proguarded jars
between proguardReleaseJars and createReleaseDistributable, so Compose
seals already-signed code.
Validated locally on arm64: clean signed createReleaseDistributable produces an
.app that passes `codesign --verify --deep --strict`, with every nested native
carrying Developer ID + hardened runtime + secure timestamp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The original "ProGuard strips the keychain backend" hypothesis turned out
to be wrong twice (PR 3260 comments document the binary PoW that refuted
both H1 strip-of-classes and H1b strip-of-native-resource). The full
117 KB osxkeychain.so resource ships intact in the proguarded
jkeychain-1.1.0-*.jar today, and Keyring.create() round-trips fine
against the proguarded classpath on macOS.
But the user-reported bug pattern (every cold boot, keychain key missing
→ forced re-login) maps so cleanly onto a hypothetical future
strip-of-native-resource that the guard is worth keeping. Cheap to run
(one unzip scan after proguardReleaseJars), wired onto every release
packaging task (DMG, MSI, DEB, RPM, current-OS distributable, runRelease)
so a regression can't slip past. Fails the build with a self-contained
explanation pointing at the next person who has to debug it.
The actual root cause of the reported bug remains unidentified after
three refuted hypotheses (see plan doc PoW table); needs the affected
user's Console.app logs + ~/.amethyst state to make further progress.
The LoginScreen "keychain-unavailable" diagnostic banner from the
earlier commit is unchanged and still earns its keep regardless of
which failure mode eventually turns out to be the cause.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Back out the hashtag-screen wiring (its `t` tag is the event category,
not a topic, so road events only matched category-named hashtags). Keep
the geohash data source + feed filter, where road events belong by their
`g` tags.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd
Add the Roadstr report (1315) and confirmation (1316) kinds to both the
relay subscription filters and the local feed gates for the hashtag and
geohash screens (the geohash kind list is shared with the home "around
me" feed). Reports carry geohash `g` tags and a category `t` tag, so they
now surface in nearby-geohash feeds and in the matching category hashtag
feed; confirmations carry `g` tags and surface in geohash feeds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd
The previous night filter used a desaturating invert whose coefficients
summed to ~1.4 per channel, which pushed every (light) OSM tone to
near-black and erased forest/water/land differentiation. Replace it with
a lightness invert composed with a 180° hue rotation: the map darkens but
hue is preserved, so forests stay dark green, water dark blue, and labels
invert to readable white.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd
osmdroid's MAPNIK tiles are always light, so in dark mode the bright map
clashed with the UI and light overlays. Apply a night-mode colour-matrix
filter to the tiles overlay when MaterialTheme.colorScheme.isLight is
false (lightness-inverted + desaturated for a clean dark-grey map, not a
plain invert that turns forests magenta). Light theme keeps normal tiles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd
The comment Text inherited LocalContentColor, which resolved to a
near-white in this card and made the report content hard to read on the
light map card. Color it onSurface so it stays readable in both themes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd
The category pill picked black/white text from a luminance threshold
(0.55) that left white text on mid-tone colors (fog gray, ice turquoise,
"other" gray), barely legible over a light map. Choose the text color by
the higher WCAG contrast ratio against the pill color instead, so light
pills now get black text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd
Redesign the kind 1315/1316 cards from a text-title-over-small-map
layout into a map-hero card: the OSM preview becomes a full-bleed hero
with a floating, color-coded category pill overlaid on it (auto-contrast
text), and the comment/status sits in a clean padded block below. Shared
RoadEventCard + CategoryPill chrome backs both the report and the
confirmation, with a graceful no-location fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd
NoteCompose's header already shows the post time and the NIP-40
expiration. The inner card's own "🕒 age · expires in …" line duplicated
that and, worse, conflicted with it: the header reads the relay-side
`expiration` tag (always created_at + 14d) while the card used the
per-type effective TTL (e.g. 30d for a speed camera), so the two showed
different countdowns for the same note. Remove the inner row; keep the
freshness-based pin dimming, which is visual-only and doesn't conflict.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd
Align Amethyst's road event cards with the roadstr reference clients
(https://github.com/jooray/roadstr) for both interoperability and a
richer presentation.
Interop:
- Match roadstr's exact emoji set: road_closure 🚫 (was ⛔) and
other ℹ️ (was 📍). The t codes and per-type TTLs already matched.
- Make the kind 1316 NIP-31 alt status-dependent ("Roadstr: event
confirmed" / "Roadstr: event denied") per the spec, instead of a
single "Roadstr: event confirmation".
Rendering:
- Colored teardrop map pin per category, using roadstr's exact color
palette, with the category emoji on the head (new MapPinIcon).
- Freshness: fade the report pin to 0.6 under 25% of effective TTL and
0.4 once effectively expired, matching roadstr's opacity rule.
- Subtitle meta line: "🕒 23m · expires in 1h" / "· Expired" on reports
and "🕒 23m" on confirmations.
- Confirmations get a green ✅ / red ❌ status pin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd
A batchSize <= 0 made the SQLite resumable reindex select no rows yet
never report done, so a caller's loop would spin forever. Clamp the page
size to at least one in both stores and add a regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZqPFds2TPPUKkMmBngwys
A full FTS rebuild can run for a long time on a big store, so add a
resumable, batched overload alongside the one-shot:
reindexFullTextSearch(resumeFrom: String?, batchSize): FtsReindexProgress
Each call processes ~batchSize events in its own write transaction and
returns an opaque cursor + done flag. The caller loops until done and may
stop at any point — the cursor is durable across crash/app-restart, and
the writer lock is released between batches, so "pause" is just "don't
make the next call". The path is additive/refresh and keeps search usable
throughout (no up-front wipe); the one-shot variant remains for a
guaranteed-clean rebuild.
- SQLite: FullTextSearchModule.reindexBatch walks event_headers ordered
by the monotonic row_id (a free, stable cursor), restricted to
searchable kinds, delete-then-insert per event so batches are
idempotent and never duplicate rows.
- Filesystem: FsEventStore walks one idx/kind/<k>/ dir per step (linear,
no re-sort); cursor is the next searchable kind. Idempotent linkFts, so
nothing is wiped. Pauses between kinds.
- Wrappers delegate; new FtsReindexProgress value type carries cursor +
progress + done.
- cli: `amy store reindex-fts` now loops the batched path to completion
and reports processed/batch counts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZqPFds2TPPUKkMmBngwys
When Tor is Active but every Tor-routed relay fails (the ExitTimeout /
RESOLVEFAILED barrage), onTorCircuitsDead() now does a warm reset() —
drop the in-process client to rebuild the circuit pool with a fresh exit
draw, while keeping guards and the consensus cache — instead of
resetWithCleanState().
The failure is exit-side, not entry-side: circuits build fine, but the
exits can't reach the relays. Wiping arti/state/ + cache can't improve
exit selection (exits aren't persisted) and only forces a ~60s cold
bootstrap — exactly the blackout that strands users on the
connection-failure dialog. A warm restart reconnects in ~5s.
The poisoned-guards safety net is preserved: the next start() re-runs
noUsableGuards(), so genuinely unusable guards are still wiped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The set of event kinds that implement SearchableEvent — and the text
each contributes via indexableContent() — is baked into the quartz
build, so it changes across app versions. Events stored under older code
keep their old (or missing) NIP-50 full-text-search rows, so search
silently misses them after an upgrade.
Add IEventStore.reindexFullTextSearch() so the app can wipe and rebuild
the FTS index from already-stored events when it has spare cycles.
Speed: only kinds that currently map to a SearchableEvent are scanned.
Kind alone selects the event class in EventFactory, so a single probe per
distinct kind is authoritative, letting us push a `kind IN (...)` filter
(SQLite) / skip whole idx/kind dirs (filesystem) so the non-searchable
bulk — reactions, zaps, follow lists — is never deserialised.
- SQLite: FullTextSearchModule.reindexAll drops+recreates the virtual
table (O(1) wipe) then streams only searchable-kind rows in one write
transaction, reusing a single INSERT statement.
- Filesystem: rebuilds only idx/fts/, driving the walk from
idx/kind/<searchable kind>/ via the new FsIndexer.linkFts.
- Wrappers (EventStore, ObservableEventStore, InterningEventStore)
delegate; the observable layer emits nothing since no event changes.
- cli: `amy store reindex-fts`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZqPFds2TPPUKkMmBngwys
Replace the reverse-geocoded "📍 City" line on the Roadstr road event
report and confirmation cards with an osmdroid OpenStreetMap preview
pinned at the event's coordinates.
- Add osmdroid-android 6.1.20 (Apache-2.0) to the version catalog and
the amethyst module.
- New LocationPreviewMap composable: an AndroidView-wrapped MapView with
MAPNIK tiles, a single marker, lifecycle-aware onResume/onPause/onDetach,
and nested-scroll-friendly touch handling so panning the map doesn't
fight the feed scroll. Sets the OSM User-Agent to the package name
(required or OSM returns 403).
- RoadEvent cards resolve a point from the explicit lat/lon tags, falling
back to the center of the finest published geohash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd