Commit Graph
15230 Commits
Author SHA1 Message Date
Claude 125947bf85 feat: modernize the New Workout composer
Give the workout composer more energy and a clearer hierarchy:

- Hero header: a large activity badge (primaryContainer circle) plus the
  activity name, both crossfading as the type changes — the form's focal point.
- Activity selector chips now carry their per-activity icon.
- Distance unit switched from two FilterChips to a Material3 segmented button.
- Leading icons on the inputs (title, distance, calories, notes) and an
  icon + label section header for Duration, for faster visual scanning.
- Roomier 16dp spacing and padding.

Pure presentation; the ViewModel and published event are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-17 00:34:37 +00:00
Claude ad1d28d0a6 chore: remove Health Connect workout detection plan doc
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-17 00:21:48 +00:00
Claude 692583cac2 feat: limit the feed workout banner to today's workouts
The Workouts feed banner now only surfaces workouts that started today; older
workouts from the past week remain reachable from the New Workout composer's
carousel. Keeps the feed focused on what just happened while still letting
people post earlier workouts on demand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 23:52:09 +00:00
Crowdin Bot 66b0428cb2 New Crowdin translations by GitHub Action 2026-06-16 23:48:41 +00:00
Vitor PamplonaandGitHub fc35f5afe3 Merge pull request #3236 from vitorpamplona/claude/fervent-pascal-dy03ve
Add share-as-image feature for notes
2026-06-16 19:46:32 -04:00
Claude 397cf7d302 feat: add a recent-workouts carousel to the New Workout composer
Show a horizontal list of workouts found in Health Connect over the last 7 days
at the top of the New Workout screen; tapping one pre-loads the form (activity,
duration, distance, calories, heart rate, steps, elevation, start time).

Unlike the feed banner, the carousel shows every workout in the window (not
filtered by what was already shared/dismissed) and re-reads on resume. The
DetectedWorkout→Route mapping and duration/relative-time formatting are
extracted to a shared file so the banner and carousel stay in sync, and the
ViewModel gains applyPrefill() to overwrite fields on tap (vs the once-only
prefill used for the nav argument).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 23:46:11 +00:00
Claude 7bad2a09b3 fix: use the Add icon on the New Workout FAB for consistency
The workout FAB used the directions_run glyph, which is visually off-center
inside the circular button and looked misaligned next to the app's other
"create" FABs. Use MaterialSymbols.Add (the + the other new-item FABs use) so it
matches; the button's size/shape/color were already identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 23:46:09 +00:00
Claude 7bed12b508 chore: add diagnostic logging to Health Connect workout reads
HealthConnectManager only logged on failure, so a successful-but-empty scan was
invisible. Add info logs (tag HealthConnectManager) for the permission check
result, the number of exercise sessions read in the window, and per session the
activity type with its mapping (or the skip reason + data origin). Makes it
possible to see why a watch workout does or doesn't surface as a suggestion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 23:34:43 +00:00
Claude a9cb2f464b fix: stop the connect card from flashing on every screen open
hasPermission started false while the async permission check ran, so the
"Share your workouts" connect card flashed for ~1s on every open before the
check confirmed permission was already granted. Make permission a tri-state
(null = not checked yet) and render nothing until it resolves, so the connect
prompt only appears once we actually know permission is missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 23:29:12 +00:00
Claude fdf7682409 feat: re-scan Health Connect when the app resumes
The suggestion only scanned once when the Workouts screen first opened, so a
workout that synced afterwards (e.g. a watch workout reaching Health Connect
while Amethyst was backgrounded) never appeared until a cold restart. Replace
the one-shot LaunchedEffect with a LifecycleResumeEffect so we scan on entry and
again every time the app returns to the foreground.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 23:23:43 +00:00
Claude 0e1233f760 fix: keep Health Connect prefs off the main thread (StrictMode)
HealthConnectStore opened SharedPreferences in its constructor, which ran during
composition on the main thread and tripped a StrictMode DiskReadViolation
(~265ms). Make the prefs lazy so construction touches no disk, and move the
read (refresh) and write (handle) onto Dispatchers.IO via the state holder's
scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 23:17:25 +00:00
Claude 723ba29006 feat: scroll the workout suggestion banner with the feed
Move the Health Connect suggestion banner from a pinned position above the feed
into the feed's LazyColumn as the first item, so it scrolls away with the list.
Adds an optional `header` slot to the shared FeedLoaded (default none, so other
feeds are unaffected) and passes the banner through WorkoutsScreen's onLoaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 23:14:20 +00:00
Claude b0bcadc6b3 fix: add Android 14+ Health Connect permission rationale activity-alias
The Connect button did nothing on Android 14+ because the permission request
fails silently without a ViewPermissionUsageActivity declaration. We only had
the pre-14 ACTION_SHOW_PERMISSIONS_RATIONALE intent-filter; add the required
activity-alias handling VIEW_PERMISSION_USAGE + HEALTH_PERMISSIONS, guarded by
START_VIEW_PERMISSION_USAGE, routed into MainActivity, so Health Connect shows
the permission dialog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 23:11:35 +00:00
Claude 03e8f86a83 fix: align Health Connect import with RUNSTR (active calories + title)
Reviewed RUNSTR's healthConnectService.ts and workoutPublishingService.ts and
matched two details so our imported kind 1301 events line up with theirs:

- Calories: RUNSTR reads ActiveCaloriesBurned; we were using TotalCaloriesBurned
  (active + basal), which over-reports. Now prefer active calories and fall back
  to total only when a source records no active energy. Adds the
  READ_ACTIVE_CALORIES_BURNED permission.
- Title: RUNSTR always emits a title, generated from the activity when none
  exists. Default the pre-filled title to the activity name so every shared
  event carries one.

The 1301 wire format already matched (distance [value,unit], HH:MM:SS duration,
lowercase exercise, plain-text content). We remain a richer superset — we also
publish avg/max heart rate and elevation_gain, which RUNSTR reads but does not
publish — and RUNSTR's lax parser ingests these without issue. Parity notes
recorded in the plan doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 23:07:42 +00:00
Claude fa2dd03be9 feat: polish share-as-image screen
- Re-layout the screen as a centered "hero" preview on a subtle gradient
  backdrop, with a soft drop shadow, instead of a top-aligned full-width
  bitmap above a settings row.
- Move the primary action into a bottom bar: a clearer server picker plus
  a full-width "Share" button with an icon. Top bar is now a plain back bar.
- Brand the captured card with a divider + Amethyst logo watermark and
  roomier padding.
- Capture in two passes (quick first preview, then a refined snapshot after
  a short settle delay) so async media has time to load into the image
  instead of showing empty placeholders.
- Show real upload progress (stage + percentage) via the shared
  UploadProgressIndicator instead of a bare spinner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ipS9wu6ffCPk5rtB8k3x
2026-06-16 23:02:33 +00:00
Claude 65d7eab85c feat: polish the Health Connect workout suggestion banner
Reworks the suggestion UI from a plain overlay into a designed, in-scroll card:

- Inline placement: the banner now sits above the feed (pushing it down) inside
  a Column rather than overlaying and covering the first feed items. The feed's
  scaffold top padding is applied to the Column and stripped from the feed to
  avoid doubling; the bottom padding (and the disappearing-bar animation) are
  preserved.
- Per-activity icon (run/ride/swim/hike/strength/yoga…) in a circular tinted
  badge, reusing the existing ExerciseType.symbol() mapping — no new glyphs.
- Metric chips (duration, distance, heart rate, calories, steps) in place of the
  single truncated summary line.
- Title shows the workout name or activity, subtitle shows a relative time;
  filled-tonal Share + text Dismiss actions; rounded 16dp cards.
- animateContentSize so dismissing a suggestion collapses smoothly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 22:51:48 +00:00
Claude 1c5c6d8a6b feat: add Compose setting to disable Health Connect workout suggestions
Adds a "Suggest workouts to share" toggle under Compose Settings (defaults on,
preserving current behavior) wired through UiSettings / UiSettingsFlow /
UiSharedPreferences. When set to NEVER, the Workouts screen no longer scans
Health Connect or shows the connect/suggestion banner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 22:33:05 +00:00
Vitor PamplonaandClaude Opus 4.8 230c247909 fix(desktop): consistent macOS .icns + multi-size Windows .ico
The macOS icon.icns shipped inconsistent artwork across its embedded
sizes — a transparent full-bleed glyph at 256/512 (shown on the DMG mount
window and Spotlight) but a white-carded glyph at 128 (shown in the Dock).
It was also missing every @2x Retina tier and its 16/32/48 entries decoded
to corrupt noise, a signature of a generic PNG->ICNS converter rather than
iconutil. Regenerate from a single transparent glyph master into a proper
iconset (all standard sizes + @2x) compositing one consistent rounded-card
look at every size, then assemble with iconutil.

The Windows icon.ico held a single 32x32 BMP, so Windows upscaled a blurry
32px everywhere it needed a larger icon. Rebuild as a multi-size .ico
(16/32/48/64/128/256, PNG-encoded) from the same glyph, full-bleed and
transparent per Windows convention (matches the Linux icon.png).

Linux icon.png is unchanged — a single transparent PNG never had the
inconsistency and Linux desktops expect transparent icons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:29:03 -04:00
Claude 6de43dec2d feat: detect workouts from Health Connect and suggest a kind 1301 post
Adds foreground auto-detection of finished workouts on Android via Google
Health Connect — the single aggregator every Android health source funnels
into (Samsung Health/Galaxy Watch, Google Fit, Fitbit, Garmin, Strava), the
same Android path RUNSTR uses. On opening the Workouts screen, Amethyst scans
Health Connect for sessions the user hasn't handled and surfaces a banner that
opens the existing workout composer pre-filled, ready to publish as a NIP-101e
WorkoutRecordEvent (kind 1301).

- HealthConnectManager reads ExerciseSessionRecord + aggregated distance,
  calories, heart rate, steps and elevation, mapping each to DetectedWorkout.
- ExerciseTypeMapper maps Health Connect activity types to NIP-101e verbs.
- HealthConnectStore remembers handled sessions per account so each is
  offered once; 7-day foreground lookback, no background service.
- WorkoutSuggestions banner: connect prompt (on-demand permission request,
  never on cold start) or detected-workout rows on the Workouts screen.
- Route.NewWorkout carries optional pre-fill; NewWorkoutViewModel publishes the
  richer metrics (heart rate, steps, elevation, start time) with
  source=health_connect (new SourceTag constant in quartz).
- androidx.health.connect:connect-client (Apache-2.0) + read-only health
  permissions and the required privacy-rationale manifest entries.

Design notes in amethyst/plans/2026-06-16-health-connect-workout-detection.md;
background ~15-min polling + notification left as a documented future seam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
2026-06-16 22:19:58 +00:00
Vitor PamplonaandClaude Opus 4.8 3a21df7775 fix(desktop): exclude leaked kotlinx-coroutines-test from release dmg
composemediaplayer 0.10.0 publishes kotlinx-coroutines-test as a runtime
dependency in its POM. That jar ships a META-INF/services registration for
kotlinx.coroutines.CoroutineExceptionHandler -> ExceptionCollectorAsService.
The release-only ProGuard pass strips the unreferenced provider class but
keeps the services manifest, so the packaged dmg crashed at startup with a
ServiceConfigurationError the first time the coroutine exception handler
loaded (DesktopHttpClient.<init>). Dev runs were unaffected since they don't
run ProGuard.

Exclude the test-only artifact so it never lands on the production classpath.
Verified: rebuilt release dmg no longer bundles the jar and launches clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:52:48 -04:00
Claude 1894d5f4b6 refactor: make share-as-image a screen with an Image preview
Convert the share-as-image flow from a dialog into a proper navigation
screen (Route.ShareNoteAsImage), reached from the note 3-dot menu. The
note is rendered off-screen into a GraphicsLayer, captured to a bitmap,
and the captured bitmap is shown as the preview via an Image composable —
so the preview is exactly what gets uploaded and shared.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ipS9wu6ffCPk5rtB8k3x
2026-06-16 21:48:06 +00:00
Vitor PamplonaandGitHub adcf71d43e Merge pull request #3234 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-16 17:35:22 -04:00
Crowdin Bot 2f09427ac6 New Crowdin translations by GitHub Action 2026-06-16 21:34:40 +00:00
Claude a8a33e830c feat: share any note as an uploaded image
Add a "Share as Image" action to the note 3-dot menu. It renders the
post into a framed card, captures it to a bitmap via a GraphicsLayer,
uploads the PNG to one of the user's Blossom media servers, and hands
the resulting URL to the Android share sheet.

The on-screen preview is the capture source, so what the user sees is
exactly what gets shared. A server spinner lets the user pick which
Blossom server to upload to, defaulting to their configured default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5ipS9wu6ffCPk5rtB8k3x
2026-06-16 21:33:06 +00:00
Vitor PamplonaandGitHub 172a8da125 Merge pull request #3233 from vitorpamplona/fix/tor-active-transition-race
fix(tor): set Active deterministically so the bootstrap callback can't race it
2026-06-16 17:32:42 -04:00
Vitor PamplonaandClaude Opus 4.8 b7dadf8ec7 fix(tor): set Active deterministically so the bootstrap callback can't race it
TorService.start() drove the Connecting→Active transition off Arti's
"Sufficiently bootstrapped" log line, gated on proxyRunning.get(). That line
is emitted from a tokio task spawned inside the native startSocksProxy
(arti-android-wrapper/lib.rs), which races startSocksProxy returning and
start() setting proxyRunning = true. When the task wins, the guard is false,
the transition is dropped, status stays Connecting, and the 60s
connection-failure screen fires even though the SOCKS proxy is up and
bootstrapped.

Set _status.value = Active(socksPort) directly in start() right after the
proxy binds and proxyRunning is set — under the lifecycleMutex start()
already holds, so a concurrent reset/stop can't clobber it. Reduce the log
callback to plain log forwarding now that it no longer drives status.

Verified on emulator-5554: Active now logged from start() before the
"Sufficiently bootstrapped" callback, zero ECONNREFUSED on the 9050 fallback
port, relays connecting through Tor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:29:41 -04:00
Vitor Pamplona 0572b0091c v1.12.0 v1.12.0 2026-06-16 14:38:13 -04:00
Vitor Pamplona 242904afd2 Merge 2026-06-16 14:33:03 -04:00
Vitor PamplonaandClaude Opus 4.8 98204eb8ff docs(changelog): cover Tor reliability + other recent v1.12.0 work
Expand the Tor entry into the full reliability cluster (warm-cache speedup,
Active-but-dead watchtower, wedged-guard recovery, 60s bootstrap timeout, dial
gating, lifecycle serialization) and add the other user-facing changes from
recent commits that weren't captured yet: pre-loaded reply threads, richer
workout cards, relay recovery after device sleep, and not resetting relay
backoff on momentary connections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:19:22 -04:00
davotoula 97e6bfb53f Merge remote-tracking branch 'upstream/main' into main-upstream 2026-06-16 20:12:07 +02:00
Vitor PamplonaandGitHub 7ae356443c Merge pull request #3232 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-16 14:09:15 -04:00
davotoula 6a497ded78 fix logging of e
use lambda
2026-06-16 20:08:03 +02:00
Crowdin Bot 65b115668b New Crowdin translations by GitHub Action 2026-06-16 17:55:35 +00:00
Vitor PamplonaandGitHub bc605095e0 Merge pull request #3229 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-16 13:53:52 -04:00
Vitor PamplonaandGitHub 707528dbf7 Merge pull request #3231 from vitorpamplona/feat/tor-circuit-health-selfheal
feat(tor): self-heal when Tor is Active but every circuit is dead
2026-06-16 13:53:43 -04:00
Vitor PamplonaandGitHub 329333a396 Merge pull request #3230 from vitorpamplona/perf/tor-preserve-consensus-cache
perf(tor): stop wiping Arti's consensus cache on every start
2026-06-16 13:53:22 -04:00
Vitor PamplonaandGitHub 9620ce3813 Merge pull request #3228 from vitorpamplona/fix/tor-lifecycle-race
fix(tor): bound Arti bootstrap with a 60s timeout + repeatable network test suite
2026-06-16 13:53:08 -04:00
Vitor PamplonaandGitHub d778e8bc4f Merge branch 'main' into fix/tor-lifecycle-race 2026-06-16 13:53:01 -04:00
Crowdin Bot 63f2e86ea3 New Crowdin translations by GitHub Action 2026-06-16 17:49:41 +00:00
Vitor PamplonaandClaude Opus 4.8 d9066572cd test(tor): add repeatable on-device networking scenario suite
The 60s bootstrap timeout this branch adds is invisible to every JVM test —
it only manifests against a real radio. Add a device-driven harness that drives
the network transitions that have historically wedged Tor and asserts the
lifecycle invariants from logcat, so the behavior can be re-verified whenever
Arti is bumped or the Tor management path changes.

tools/tor-network-tests/run.sh — adb-driven runner, one function per scenario,
PASS/FAIL per check, restores a clean network state on exit:
  - cold_start        Active reached + no pre-ready dial storm (the #3223 gate)
  - offline_bootstrap empty cache + airplane: asserts the bootstrap is bounded
                      ('bootstrap timed out' logged), then recovers on restore.
                      This is the regression test for this PR — it FAILS on a
                      build without the timeout (create_bootstrapped blocks 95s+).
  - wifi_cellular     WiFi->Cellular handover recovers to Active
  - airplane          offline pauses relays cleanly; restore recovers
  - pause_resume      backgrounding winds relays down (~30s); resume reconnects

README.md documents each scenario's rationale, device setup, when to re-run
(new Arti version, TorService/TorManager/dial-gate changes), and how the suite
relates to TorManagerTest / TorCircuitHealthTrackerTest / the instrumented test.

Verified on a Pixel emulator (WiFi + Cellular): all scenarios pass on this
branch; offline_bootstrap fails on main (no timeout), confirming the suite
discriminates fixed from broken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:44:06 -04:00
davotoula 67a39ac51e update cs,sv,de,pt 2026-06-16 19:39:20 +02:00
Vitor PamplonaandClaude Opus 4.8 829afba141 fix(tor): require a sustained failure streak before circuit self-heal
On-device testing caught a false positive: the original 8-failures-in-a-30s-
window detector fired ~2s after Tor flipped Active. The instant Tor goes Active
the pool dials the whole Tor-routed relay set at once, and on freshly built
circuits a burst of >=8 can fail before the first relay completes its handshake
— so the detector wiped the good, just-bootstrapped client.

Replace the count-window with a sustained-streak model: fire only when an
unbroken run of Tor-routed failures both reaches FAIL_THRESHOLD and spans at
least SUSTAINED_MS (30s), with zero successes interrupting it (any success ends
the streak). A gap longer than SUSTAINED_MS also restarts the streak. The span
floor doubles as a post-Active warmup grace, so the warmup burst — and the
identical reconnection burst on app resume — no longer trips it.

Verified on device across cold start, WiFi<->Cellular handover, airplane
on/off, and app pause/resume: 0 false self-heals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:32:42 -04:00
Vitor PamplonaandClaude Opus 4.8 eaf07fd4ad feat(tor): self-heal when Tor is Active but every circuit is dead
TorManager's watchdogs (selfHealSignal, connectionFailure) only arm while
status is Connecting. Once Arti logs "Sufficiently bootstrapped" off cached
consensus, status flips to Active and every watchdog disarms — even if no exit
circuit actually works (the ExitTimeout/RESOLVEFAILED barrage seen on device).
The SOCKS proxy is up, so nothing in the lifecycle looks wrong, and Tor can sit
Active-but-useless indefinitely with no recovery.

The relay layer is the only place with both halves of the signal: per-relay
success (onConnected) and failure (onCannotConnect), plus the Tor-routing of
each url. Arti emits no per-stream success log, so a Tor-internal trigger could
only count failures — and couldn't tell "all dead" from "some dead", resetting
Tor every time a few dead relays are dialed.

Add TorCircuitHealthTracker, a RelayConnectionListener that fires only when,
while Tor is Active and connectivity is up, there are >= FAIL_THRESHOLD (8)
Tor-routed failures within WINDOW_MS (30s) AND zero Tor-routed successes in that
window. The zero-success clause is the whole discriminator: one successful Tor
open means circuits work, so it suppresses. It pokes TorManager.onTorCircuitsDead(),
which mirrors the post-Active stuck-Connecting recovery — resetWithCleanState()
(wipe the suspect guard/circuit state behind a healthy-looking guards.json) plus
a resetEpoch bump to force a full re-init — and shares lastSelfHealAtMs /
SELF_HEAL_COOLDOWN_MS with the Connecting watchdog so the two can't thrash. If
circuits are still dead after the reset, the cooldown suppresses further resets
and the 60s connectionFailure dialog still offers the user the bypass.

Tests: TorManagerTest covers onTorCircuitsDead (fires + wipes + re-inits when
Active; no-op while not-Active / bypassing; shares the cooldown).
TorCircuitHealthTrackerTest covers the discriminator (threshold, single-success
disarm, clearnet ignored, connectivity/Active gating, window aging, re-arm).

Verified on device: no crash, tracker stays silent during normal operation
(168 relays opened, 0 self-heal fires).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:10:56 -04:00
Vitor PamplonaandClaude Opus 4.8 4f668f6175 perf(tor): stop wiping Arti's consensus cache on every start
TorService.start() called clearArtiCache() on every cold start (and every
reset, since reset flips initialized back to false), deleting the
consensus/microdescriptor cache and forcing a full re-download on the next
bootstrap. Measured on device, that turned a ~7.7s warm bootstrap into ~24.2s
— a ~3.2x slowdown paid on every launch.

The wipe was an early attempt at what we later understood to be the wedged-
guard problem (now handled by noUsableGuards()/clearAllArtiData()). It never
actually helped: guards live in state/, not cache/, so wiping the cache can't
fix a stale guard sample; and Arti already validates consensus freshness and
refetches whatever has expired, so there is no stale-consensus risk to guard
against here. The reset/clean-state self-heal paths still call
clearAllArtiData() for genuine corruption recovery.

Drop clearArtiCache() entirely and preserve the cache for warm bootstraps.
Also add bootstrap-timing instrumentation: the "SOCKS proxy active" log now
reports elapsed bootstrap ms, and a cache-size log line correlates cache
state with bootstrap time.

Verified on device: warm bootstrap 7,670ms vs 24,200ms cold, with no
regression — a .onion relay and 200 clearnet relays connected over Tor, 0
pre-ready doomed dials, guards healthy (59/60 usable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:57:50 -04:00
Vitor PamplonaandGitHub 40921b80d2 Merge pull request #3227 from vitorpamplona/claude/eager-keller-9a4exu
Eagerly preload thread for replies visible in feed
2026-06-16 11:06:30 -04:00
Vitor PamplonaandGitHub 63ef0a3fb6 Merge pull request #3226 from vitorpamplona/claude/upbeat-hypatia-klvd5d
Redesign workout display with hero metric and stats grid
2026-06-16 11:04:55 -04:00
Vitor PamplonaandGitHub bfb41b8d3d Merge pull request #3224 from vitorpamplona/fix/tor-lifecycle-race
fix: Tor lifecycle race + bounded bootstrap so a hostile network can't wedge Tor
2026-06-16 10:46:26 -04:00
Vitor PamplonaandClaude Opus 4.8 69b8ba9f23 fix: bound Arti bootstrap with a 60s timeout so a hostile network can't wedge Tor
A self-heal reset()/resetWithCleanState() (and the lifecycle serialization added
in the previous commit) can only recover Tor if initialize() actually returns.
ArtiNative.initialize() calls TorClient::create_bootstrapped, which on a hostile
network (unreachable guards, wiped consensus) retries internally for many
minutes. While it blocks it holds lifecycleMutex, so the watchdog's reset can
never run — Tor stays wedged at Connecting.

Wrap create_bootstrapped in a 60s tokio::time::timeout. On timeout the future is
dropped (tearing down the half-built client) and initialize() returns -4; the
JNI ABI is unchanged (still one String arg), so the checked-in CI host .so and
TorArtiNativeIntegrationTest keep working without a rebuild. TorService treats
-4 specially: drop the init flag and leave status Connecting (don't wipe+retry
inline under the lock, don't go Off) so TorManager's self-heal watchdog resets
and re-inits on its own cadence, and connectionFailure can still surface the
"use regular connection" dialog.

Rebuilt libarti_android.so for arm64-v8a + x86_64.

Verified on device: a no-network cold-start bootstrap timed out at exactly 60s
(previously hung 7+ min), released the lock, and on network restore the watchdog
re-init'd and Tor reached Active. Addresses #3225.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:41:09 -04:00
Vitor PamplonaandClaude Opus 4.8 bdfc56cf23 fix: serialize Arti lifecycle so a reset can't destroy a bootstrapping client
ArtiNative is a process-global singleton over a single native Tor client, and
initialize/destroy are blocking JNI calls that ignore coroutine cancellation.
TorService guarded it with only two AtomicBooleans and no mutual exclusion
across start/stop/reset/resetWithCleanState. A self-heal reset() (stuck-
Connecting watchdog or onNetworkChange) could therefore call destroy() while a
start() was mid-initialize(): the freshly bootstrapped client was destroyed
~0.5s after coming up, leaving the SOCKS listener bound with no live client
behind it. Every Tor dial then timed out at the exit (ExitTimeout barrage) and
status was left at a stale Active.

Serialize all four native lifecycle transitions behind a single lifecycleMutex
so destroy() can never overlap initialize() — a reset now waits for an
in-flight bootstrap to finish before tearing it down cleanly. reset() and
resetWithCleanState() share a private resetLocked() helper (Mutex is not
reentrant). Also gate the "Sufficiently bootstrapped" -> Active callback on
proxyRunning so a late callback from a torn-down client can't resurrect a stale
Active status.

Verified on device: forced reset+start now fully serialize (destroy completes
before create begins), and a clean start bootstraps to Active in one cycle with
no churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:06:59 -04:00
Vitor PamplonaandGitHub ff2de15b20 Merge pull request #3223 from vitorpamplona/fix/gate-tor-relays-until-ready
Gate Tor-routed relay dials until Tor is ready
2026-06-16 09:44:53 -04:00