Skip to content

More robust SignalR connection handling#2317

Open
myieye wants to merge 25 commits into
developfrom
signalr-retry-improvements-v2
Open

More robust SignalR connection handling#2317
myieye wants to merge 25 commits into
developfrom
signalr-retry-improvements-v2

Conversation

@myieye

@myieye myieye commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Resolves #2331 — production symptoms and log evidence live there.

From manual testing, syncing (both pulling and pushing) appear to recover very quickly on connectivity and auth changes, even when opening projects when offline or unauthenticated, which previously caused trouble. The sync dialog also refreshed and keeps state info up-to-date. Tested most thoroughly on Android, but also on desktop (MAUI).

FwLite listens for Lexbox project-change push notifications over a per-server SignalR HubConnection. After network loss or an OS-frozen app (Android Doze) that connection could end up dead-but-cached: opening a project occasionally threw SendCoreAsync … not active, but more often push just silently stopped until a manual sync or restart. This makes the whole lifecycle robust, on the FwLite client side.

Don't get stuck. Check auth before reconnecting or handing back a cached connection; abort the infinite retry loop only on a real logout — decided by local account presence (IsSignedIn), not by whether a token can be fetched mid-outage, which was a false-logout trap. Guard Closed/eviction with ReferenceEquals so a live replacement is never evicted, tolerate SendAsync racing a drop, dispose on failed StartAsync, and serialize connection creation per server so concurrent callers can't build duplicates that dispose each other mid-use.

Always recover. A new _trackedProjects map (kept across rebuilds) lets listeners be re-established from every angle: auth change, successful sync, MAUI connectivity-regained, MAUI app-resume, and a 5-minute periodic backstop. All idempotent, and healing is per-project so siblings on a rebuilt connection get resubscribed rather than silently dropped.

Recover faster. Reconnect backoff adapts to device-reported network state (retry ~10s while online, back off to 60s while offline); and because SignalR has no retry-now API, strong recovery signals (connectivity/resume/post-sync) interrupt a mid-backoff reconnect instead of waiting it out.

Show offline, don't error. When a sync can't reach the server — connection drop or timeout — it now reports an Offline status instead of surfacing a raw connection error, and the FwLite sync dialog shows a brief offline notice while leaving the pending-change counts intact rather than optimistically zeroing them.

Be diagnosable. HubConnection's own logs now flow into the app logger, so the next occurrence is debuggable from user logs.

Depended on #2308 (merged) — the "logged out" signal is only trustworthy with those OAuth fixes in place. New/updated unit tests cover the reconnect decision, adaptive backoff, eviction guards, and resubscribe bookkeeping; the connection-orchestration paths that need a live HubConnection are exercised manually (cold-start network isolation needs per-process network control, not in CI). Manual check for the offline path: go offline, press Sync → the dialog shows the offline notice and the pending-change counts are preserved.

Housekeeping in this PR: .editorconfig silences IDE0061/IDE0022 for the expression-bodied members added here, and .gitignore excludes *.props.user and a local-only signalr-acceptance-tests.md manual-test cookbook.

Known follow-up: _trackedProjects is process-lifetime with no untrack path — a project opened once keeps being resubscribed for the session (bounded by distinct projects opened, not per-open). Intentional for now; revisit if listeners should stop on project-close.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds network-aware SignalR push-listener recovery for MAUI and web: introduces an INetworkStatus abstraction, refactors LexboxProjectService with a tracked-project registry and AdaptiveRetryPolicy, and adds new hosted services (ConnectivitySyncTrigger, NetworkChangeSyncTrigger, PushListenerRecoveryService, AuthChangeListenerTrigger) that trigger listener reconnection on network/auth/resume events. SyncService gains an offline early-return gate and CrdtHttpSyncService gains health-cache invalidation. The frontend SyncDialog shows a "local changes safe" warning toast on unsynced results.

Changes

SignalR Push Listener Recovery

Layer / File(s) Summary
INetworkStatus interface and platform implementations
backend/FwLite/FwLiteShared/Services/INetworkStatus.cs, backend/FwLite/FwLiteShared/Services/NetworkInterfaceNetworkStatus.cs, backend/FwLite/FwLiteMaui/Services/ConnectivityNetworkStatus.cs
Defines INetworkStatus with bool IsOnline. NetworkInterfaceNetworkStatus (web/shared default) uses NetworkInterface.GetIsNetworkAvailable(). ConnectivityNetworkStatus (MAUI) uses IConnectivity.NetworkAccess == Internet.
LexboxProjectService refactor: tracked projects, adaptive retry, auth handling
backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs
Removes IDisposable and GlobalEventBus subscription. Adds _trackedProjects registry, ILoggerFactory/INetworkStatus injection, HandleAuthChanged, EnsureListenersForTrackedProjects, and kickReconnecting support. Replaces constant infinite retry with AdaptiveRetryPolicy. Adds EvictAndStopIfCached with identity guard, HandleReconnecting with connection-identity checks, per-server SemaphoreSlim gates, and updated BuildAndStartConnection lifecycle callbacks.
Recovery hosted services and DI wiring
backend/FwLite/FwLiteShared/Projects/AuthChangeListenerTrigger.cs, backend/FwLite/FwLiteShared/Projects/PushListenerRecoveryService.cs, backend/FwLite/FwLiteMaui/Services/ConnectivitySyncTrigger.cs, backend/FwLite/FwLiteWeb/Services/NetworkChangeSyncTrigger.cs, backend/FwLite/FwLiteMaui/App.xaml.cs, backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs, backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs, backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs
AuthChangeListenerTrigger subscribes to GlobalEventBus.OnAuthenticationChanged and calls HandleAuthChanged. PushListenerRecoveryService runs EnsureListenersForTrackedProjects on a 5-minute PeriodicTimer. ConnectivitySyncTrigger (MAUI) triggers recovery on internet-access transitions. NetworkChangeSyncTrigger (web) triggers recovery on NetworkAvailabilityChanged. App.xaml.cs adds a Resumed-event hook. All registered in kernel DI.
SyncService offline gate and CrdtHttpSyncService health-cache invalidation
backend/FwLite/FwLiteShared/Sync/SyncService.cs, backend/FwLite/LcmCrdt/RemoteSync/CrdtHttpSyncService.cs
SyncService.ExecuteSync gains an early offline return via INetworkStatus.IsOnline; mid-sync HttpRequestException/OperationCanceledException are mapped to Offline. UploadProject throws on non-synced result. CrdtHttpSyncService extracts HealthCacheKey helper and adds InvalidateServerHealth to evict a cached verdict.
Tests: LexboxProjectService, AdaptiveRetryPolicy, ConnectivitySyncTrigger, CrdtHttpSyncService
backend/FwLite/FwLiteShared.Tests/Projects/LexboxProjectServiceTests.cs, backend/FwLite/FwLiteShared.Tests/Projects/AdaptiveRetryPolicyTests.cs, backend/FwLite/FwLiteMaui.Tests/ConnectivitySyncTriggerTests.cs, backend/FwLite/LcmCrdt.Tests/CrdtHttpSyncServiceTests.cs
LexboxProjectServiceTests covers cached-connection reuse, HandleReconnecting sign-in/logout/replacement cases, EvictAndStopIfCached guards, and RegisterForResubscribe. AdaptiveRetryPolicyTests covers immediate early attempts, online/offline delays, per-attempt re-evaluation, and never-null guarantee. ConnectivitySyncTriggerTests covers ShouldRecover transition matrix. CrdtHttpSyncServiceTests covers health caching and InvalidateServerHealth.
Frontend SyncDialog warning toast and locale strings
frontend/viewer/src/project/SyncDialog.svelte, frontend/viewer/src/locales/*.po
SyncDialog.svelte checks result.isSynced instead of falsy return, showing a "local changes are safe" warning and calling refreshStatus(). New msgid for that toast added to 8 locale files (en, es, fr, id, ko, ms, sw, vi); "Failed to synchronize" source reference updated across those files.
Tooling config
.gitignore, backend/.editorconfig
.gitignore adds *.props.user and signalr-acceptance-tests.md. .editorconfig silences IDE0061/IDE0022 and repositions trim_trailing_whitespace.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • hahn-kev

Poem

🐇 The network went dark, the listeners fell asleep,
But now a brave rabbit has promises to keep!
On resume, on auth-change, on signal regained,
The hubs reconnect swiftly — no changes are maimed.
"Your local edits are safe!" the toast kindly says,
While AdaptiveRetryPolicy counts offline days. 🌐✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'More robust SignalR connection handling' is closely related to the main change—addressing production failures in SignalR/HubConnection lifecycle management.
Description check ✅ Passed The description is comprehensive and directly addresses the changeset, explaining network recovery, auth handling, offline presentation, and the recovery mechanisms implemented.
Linked Issues check ✅ Passed The PR successfully addresses issue #2331: implements multi-path recovery (auth change, sync, connectivity, resume, periodic backstop), adaptive backoff based on network state, offline status reporting, robust lifecycle management with proper eviction guards, and per-project healing—all key objectives identified in the issue.
Out of Scope Changes check ✅ Passed All changes are within scope: core improvements to SignalR/listener recovery, new hosted services for recovery triggers, adaptive retry policy, offline detection, plus housekeeping in .editorconfig and .gitignore directly related to the implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch signalr-retry-improvements-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the 💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related label May 29, 2026
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

UI unit Tests

  1 files  ±0   62 suites  ±0   32s ⏱️ +5s
186 tests ±0  186 ✅ ±0  0 💤 ±0  0 ❌ ±0 
258 runs  ±0  258 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 4177009. ± Comparison against base commit fa14cfa.

♻️ This comment has been updated with latest results.

@argos-ci

argos-ci Bot commented May 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - Jun 17, 2026, 4:21 PM

myieye and others added 16 commits June 16, 2026 14:01
ListenForProjectChanges previously registered a project in the
_reconnectProjects table only AFTER its state-based early returns. A
project subscribed while the underlying HubConnection was Reconnecting
would silently never be resubscribed when the connection came back.
Hoist the registration above the state checks so the comment ("Reconnected
handler will resubscribe...") is actually true.

Also extract the Reconnecting auth-loss decision into a static internal
method so the auth-stop behavior can be unit-tested without a real
HubConnection or full DI graph. Add two unit tests covering token-present
(no-op) and token-null (evict + stop) branches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously OnAuthenticationChanged only invalidated the cached project
list — the hub connection stayed up with whatever token it was built
with. After a logout/login cycle the listener kept running with stale
auth until the next reconnect attempt happened to detect it.

Track each project we're asked to listen for in a stable dictionary
(separate from the per-connection _reconnectProjects table) so we have a
list to re-subscribe even after the underlying HubConnection has been
torn down. On every auth event, evict the cached connection and walk
that list to (re)start listeners; on logout the new connections fail
fast at the auth check, on login they come up with the fresh token —
auto-recovery without waiting for the user to manually sync or reopen
the project.

EvictAndStopIfCached is extracted as a static internal helper to match
the HandleReconnecting pattern; add two unit tests for its empty-cache
and cached-connection branches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
If the initial StartLexboxProjectChangeListener call fails (e.g. user
was offline at project-open), the WithAutomaticReconnect policy never
kicks in — the listener stays dead until something re-triggers
ListenForProjectChanges. Today the only triggers are project open and
the FwLiteWeb hub's OnConnectedAsync; a long-running session can sit
indefinitely with no push notifications.

Fire-and-forget a ListenForProjectChanges call after every successful
SyncService.ExecuteSync. If the listener is already running the cache
short-circuits and it's effectively free; if it died (cold-start
failure, transient network) this is the recovery trigger. Wrap in
try/catch so a listener problem can't masquerade as a sync failure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When two ListenForProjectChanges callers race past the cache check (both
see it empty), both build a HubConnection. The second wins the cache
slot; the first is evicted via the IMemoryCache eviction callback and
disposed. Disposing fires its Closed event handler, which unconditionally
called cache.Remove — evicting the *replacement* connection that just
took its place. The next ListenForProjectChanges call finds an empty
cache and races again.

The race is exposed (not caused) by the post-sync piggyback firing
ListenForProjectChanges on every successful sync — observed during
two-instance manual testing as a cascade where the cache stayed empty
and connections kept cycling. Guard the cache eviction with a
ReferenceEquals check so an orphaned Closed event can't take down the
live cached connection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Dispose the HubConnection when StartAsync fails (was leaked on cold-start retry)
- Evict a dead cached connection if Disconnected-recovery StartAsync fails
- Broaden the subscribe SendAsync catch so a mid-call drop can't fail project-open
- Extract RegisterForResubscribe and unit-test the subscription-ordering invariant
- HandleReconnecting takes a bool; null token = logged out (OAuthClient owns transient-vs-logout)
- MAUI: ConnectivitySyncTrigger re-establishes listeners when connectivity returns

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PushListenerRecoveryService re-runs EnsureListenersForTrackedProjects
every 5 minutes so a listener that failed to start (e.g. offline at
project-open) recovers without a manual sync, on platforms with no
connectivity events (FwLiteWeb). Servers with a live connection are
skipped via a pre-loop snapshot so the periodic pass is a no-op once
connected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… recovery

Two paths could leave sibling projects silently unsubscribed while their
server reported Connected: a manual revive (StartAsync does not raise
Reconnected) resubscribed only the requesting project, and the recovery
pass skipped whole servers, so a connection rebuilt by a single-project
path (post-sync, project-open) never picked the siblings back up. Extract
the Reconnected handler body into ResubscribeRegisteredProjects, call it
after a successful revive, and make EnsureListenersForTrackedProjects skip
per project (connected AND registered) instead of per server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HandleReconnecting removed the cache entry by key, and the revive-failure
path evicted whatever was cached — either could race a rebuild and evict a
live replacement connection, orphaning it while callers believe the server
has no listener. Apply the same ReferenceEquals guard the Closed handler
uses: HandleReconnecting only evicts its own entry (still stops itself —
it has no token), and EvictAndStopIfCached takes an optional expected
instance for callers holding a specific dead connection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Concurrent ListenForProjectChanges callers that both missed the cache each
built and started a connection; the cache replacement then disposed the
loser mid-use and silently discarded its resubscribe registrations.
Concurrent revives of the same Disconnected connection were worse: the
losing StartAsync threw and evicted the winner. A per-server gate around
build-and-cache and revive (with re-checks inside) leaves one connection
per server by construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The HubConnection logged only to its own console-only factory, so
reconnect attempts and close reasons never reached the app log file --
production logs could not answer why a connection died. Register the app
logger factory in the builder''s service collection so SignalR client logs
flow through the app''s providers and filters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An OS-frozen app (Android Doze) can return to the foreground with its
push listener down and no connectivity transition for the connectivity
trigger to react to, leaving recovery to the 5-minute periodic backstop.
Resume is exactly when the user is watching, so recover immediately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fixed backoff topped out at 60s, and since SignalR cannot be hurried
mid-delay, that was the worst-case window where HTTP already works but
pushes are still down (e.g. after a server deploy, where no client-side
signal exists to trigger recovery). Retry every 10s while the device
reports online -- the failure is server-side and could end any moment --
and back off to 60s while offline, where every attempt is a pointless
local failure. Hosts without a connectivity API (FwLiteWeb) report
always-online.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A connection sleeping in its reconnect backoff cannot be hurried -- every
recovery mechanism found it Reconnecting and correctly stood down, so the
committed delay was the floor on recovery latency even when the network
was demonstrably back. Callers with strong evidence (connectivity
regained, app resume, a successful sync to that server) now stop the
mid-backoff connection and rebuild through the normal serialized path,
which also resubscribes every tracked project. Weak-signal callers (the
periodic backstop) never kick: a wrong kick trades the automatic retry
loop for the much slower backstops.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ndler

Remove the StartAsync retry TODO (post-sync, connectivity, resume and
periodic recovery now cover it), fix comments referencing the renamed
retry policy, and catch TriggerSync failures in the OnProjectUpdated
handler -- it throws when the background sync service is not running and
SignalR would swallow that into its own logger.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Reconnecting handler decided "logged out" from whether a token could be
fetched right then -- but reconnecting happens precisely when the network is
flaky, and fetching a token can require a refresh round-trip that fails
transiently, which read as a false logout and tore down a listener that
should have kept retrying. Switch the signal to IsSignedIn (a local-only
account-presence read that flips only on a real logout), now available from
the merged OAuth fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@myieye myieye force-pushed the signalr-retry-improvements-v2 branch from c498cb3 to 29c91a8 Compare June 16, 2026 14:06
@github-actions github-actions Bot added the 📦 Lexbox issues related to any server side code, fw-headless included label Jun 16, 2026
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

C# Unit Tests

165 tests   165 ✅  23s ⏱️
 23 suites    0 💤
  1 files      0 ❌

Results for commit 4177009.

♻️ This comment has been updated with latest results.

Move auth-change listener recovery out of LexboxProjectService into a dedicated
AuthChangeListenerTrigger hosted service, so the subscription lifecycle no longer
rides on the service's IDisposable. Report connectivity from the OS network-interface
table (NetworkInterfaceNetworkStatus) rather than assuming always-online, and add
NetworkChangeSyncTrigger so web hosts re-ensure listeners when connectivity returns
(the cross-platform counterpart to MAUI's ConnectivitySyncTrigger).

StartLexboxProjectChangeListener now checks the connection cache before any auth call
and gates a new build on IsSignedIn (account presence) instead of GetCurrentToken: a
transient token-refresh failure during a reconnect could otherwise be mistaken for a
logout and tear down a healthy auto-reconnecting connection (the same false-logout trap
HandleReconnecting already avoids). Adds a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@myieye myieye force-pushed the signalr-retry-improvements-v2 branch from 29c91a8 to 40068a1 Compare June 16, 2026 14:37
myieye and others added 3 commits June 16, 2026 17:02
Each recovery path now announces itself: the auth-change, MAUI-connectivity and
web-network-availability triggers log at startup that they're watching, the MAUI
resume hook logs when it fires, and the periodic backstop logs a debug heartbeat
per check. The connectivity/network triggers already logged their fire events;
auth-change fire detail stays in HandleAuthChanged. Makes it easier to tell which
of the several recovery paths did (or didn't) run when debugging listener issues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ne verdict

A health check that failed while offline was cached as unhealthy for 30 minutes,
so a sync triggered by network recovery (this branch's reconnect-sync) returned the
stale "unhealthy" verdict and refused to sync until the cache expired or the app
restarted. On a genuine connect transition — where SignalR reaching the server proves
it's reachable — drop the cached health verdict (and the per-server projects list,
which has the same stale-offline-failure problem) so the catch-up sync re-probes.

Also quiet the expected ObjectDisposedException when stopping an evicted connection
that's mid-reconnect (it has already disposed itself; logged at Debug), and downgrade
the per-attempt reconnect log to Debug.

Verified on Android: cold-start-while-offline then online now syncs within seconds,
where it previously stayed "Offline" with pending downloads for the full cache window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….user

Reframe the connectivity-change log (was a temporary diagnostic) as a permanent
event so real-device behaviour stays legible, and gitignore the local-harmony
*.props.user artifact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

C# FwHeadless Unit Tests

48 tests   48 ✅  16s ⏱️
 5 suites   0 💤
 1 files     0 ❌

Results for commit 4177009.

♻️ This comment has been updated with latest results.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Caution

Review failed

An error occurred during the review process. Please try again later.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch signalr-retry-improvements-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

myieye and others added 4 commits June 17, 2026 16:43
…rror

When the device was offline the sync indicator/dialog stayed green and a manual
sync surfaced a raw UnknownHostException: the server-health cache can still report
healthy from an earlier online sync, so ExecuteSync sailed past the ShouldSync gate
and the actual transfer threw — which bubbled to the UI and skipped the offline
status update.

- ExecuteSync now trusts the device's network state: if offline, report Offline and
  return without attempting the transfer; also catch a mid-sync HttpRequestException
  and map it to Offline. Either path fires an Offline sync event, so the indicator
  dot and the sync dialog reflect offline.
- SyncDialog handles an un-synced result gracefully (gentle "you're offline" toast,
  no optimistic zeroing of pending counts) rather than letting it reach the global
  error handler.

Verified on Android: an offline sync now shows "Offline" + cloud-off with a friendly
toast; going back online still syncs normally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Widen the ExecuteSync catch to OperationCanceledException so an HttpClient
timeout reads as Offline instead of leaking a raw error. ExecuteSync takes no
cancellation token, so an OCE there is always a transport timeout, never a
caller abort.

The CRDT sync dialog showed "You're offline" for every non-synced outcome
(also NoServer/NotLoggedIn); use a status-neutral message and drop a dead
null-guard. The persistent status indicator still shows the specific status.
Re-extract catalogs and add translator context for the new string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ExecuteSync now reports offline/connectivity failures as an unsynced result
instead of throwing, so UploadProject's catch no longer fired on a failed
upload: the project was left bound to a server it never reached and the
/upload route returned Ok(). Honor the unsynced result so the origin is rolled
back and the failure surfaces, restoring the pre-offline-handling contract.

Found by Devin review of #2317.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… too

The PushListenerRecoveryService comment claimed the connectivity trigger was
MAUI-only and that FwLiteWeb had none, but this branch added
NetworkChangeSyncTrigger for exactly that. Correct it, and state the real
reason the periodic backstop still matters: the OS network signal is optimistic
(a virtual adapter reads "available"), so a real-uplink return may not fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@myieye myieye marked this pull request as ready for review June 17, 2026 16:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs (1)

32-35: 🏗️ Heavy lift

Add an untrack path for _trackedProjects to prevent stale recovery/sync fan-out.

_trackedProjects is session-lifetime and currently only grows. Over long sessions, closed/removed projects will still be iterated by periodic recovery and reconnect sync triggers, which can create avoidable background work. Consider adding an explicit unregistration hook on project close/removal and pruning dead entries.

Also applies to: 395-401

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs` around lines 32
- 35, The _trackedProjects ConcurrentDictionary grows indefinitely over the
session lifetime and never removes closed or removed projects, causing
unnecessary iteration and background work in periodic recovery and reconnect
sync operations. Add an explicit untracking method that removes entries from
_trackedProjects when projects are closed or removed, and call this method in
the appropriate project lifecycle handlers (project close/removal events or
callbacks). This ensures dead entries are pruned and don't create avoidable
background work during periodic recovery and reconnect sync triggers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs`:
- Around line 32-35: The _trackedProjects ConcurrentDictionary grows
indefinitely over the session lifetime and never removes closed or removed
projects, causing unnecessary iteration and background work in periodic recovery
and reconnect sync operations. Add an explicit untracking method that removes
entries from _trackedProjects when projects are closed or removed, and call this
method in the appropriate project lifecycle handlers (project close/removal
events or callbacks). This ensures dead entries are pruned and don't create
avoidable background work during periodic recovery and reconnect sync triggers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: df2671f8-6e2c-4bd2-a51e-a0202d3ef5f2

📥 Commits

Reviewing files that changed from the base of the PR and between fa14cfa and 4177009.

📒 Files selected for processing (29)
  • .gitignore
  • backend/.editorconfig
  • backend/FwLite/FwLiteMaui.Tests/ConnectivitySyncTriggerTests.cs
  • backend/FwLite/FwLiteMaui/App.xaml.cs
  • backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs
  • backend/FwLite/FwLiteMaui/Services/ConnectivityNetworkStatus.cs
  • backend/FwLite/FwLiteMaui/Services/ConnectivitySyncTrigger.cs
  • backend/FwLite/FwLiteShared.Tests/Projects/AdaptiveRetryPolicyTests.cs
  • backend/FwLite/FwLiteShared.Tests/Projects/LexboxProjectServiceTests.cs
  • backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs
  • backend/FwLite/FwLiteShared/Projects/AuthChangeListenerTrigger.cs
  • backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs
  • backend/FwLite/FwLiteShared/Projects/PushListenerRecoveryService.cs
  • backend/FwLite/FwLiteShared/Services/INetworkStatus.cs
  • backend/FwLite/FwLiteShared/Services/NetworkInterfaceNetworkStatus.cs
  • backend/FwLite/FwLiteShared/Sync/SyncService.cs
  • backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs
  • backend/FwLite/FwLiteWeb/Services/NetworkChangeSyncTrigger.cs
  • backend/FwLite/LcmCrdt.Tests/CrdtHttpSyncServiceTests.cs
  • backend/FwLite/LcmCrdt/RemoteSync/CrdtHttpSyncService.cs
  • frontend/viewer/src/locales/en.po
  • frontend/viewer/src/locales/es.po
  • frontend/viewer/src/locales/fr.po
  • frontend/viewer/src/locales/id.po
  • frontend/viewer/src/locales/ko.po
  • frontend/viewer/src/locales/ms.po
  • frontend/viewer/src/locales/sw.po
  • frontend/viewer/src/locales/vi.po
  • frontend/viewer/src/project/SyncDialog.svelte

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related 📦 Lexbox issues related to any server side code, fw-headless included

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FwLite push listener doesn't recover after network loss / OS suspend; project-open can crash ('SendCoreAsync … not active')

1 participant