import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/events.dart'; import 'package:aves/remote/remote_settings.dart'; import 'package:aves/remote/remote_sync_bus.dart'; import 'package:aves/remote/remote_repository.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/remote/remote_client.dart'; import 'package:aves/remote/auth_client.dart'; import 'package:aves/remote/collection_source_remote_ext.dart'; import 'package:aves/remote/collection_source_remote_ws_ext.dart'; import 'remote_origin.dart'; import 'remote_http_api.dart'; import 'remote_sync_engine.dart'; import 'remote_state_store.dart'; import 'remote_ws_client.dart'; import 'remote_http.dart'; import 'remote_added_queue.dart'; class RemoteController { RemoteController._(); static final RemoteController instance = RemoteController._(); static const _kBootstrapDone = 'remote_bootstrap_done'; bool _syncInFlight = false; DateTime? _retryStartTime; Timer? _retryTimer; RemoteAuth? _auth; RemoteHttpApi? _api; RemoteSyncEngine? _engine; RemoteWsClient? _ws; RemoteStateStore? _stateStore; RemoteAddedQueue? _addedQueue; EventRingDeduper? _deduper; Future? _ensureStackFuture; bool _wsConnectInFlight = false; // coalescing progressive Timer? _progressiveTimer; bool _progressiveScheduled = false; String? _pendingSinceIso; bool _progressiveInFlight = false; // ------------------------------------------------------------ // helper: forza refresh lens quando cambia remoteVisible // ------------------------------------------------------------ void _notifyRemoteVisibilityChanged(CollectionSource source) { try { source.eventBus.fire(const FilterVisibilityChangedEvent()); } catch (_) { // ignore } } // refresh “forte” (la lens ascolta EntryRefreshedEvent e fa refresh()) void _forceCollectionRefresh(CollectionSource source) { try { source.eventBus.fire(const EntryRefreshedEvent({})); } catch (_) { // ignore } } Future _waitSourceReady( CollectionSource source, { required String reason, Duration timeout = const Duration(seconds: 45), }) async { if (source.isReady) { debugPrint( '[remote_controller] source already ready ' 'reason=$reason ' 'state=${source.state}', ); return; } debugPrint( '[remote_controller] waiting source ready ' 'reason=$reason ' 'state=${source.state}', ); final completer = Completer(); Timer? timer; late VoidCallback listener; listener = () { if (source.isReady && !completer.isCompleted) { completer.complete(); } }; source.stateNotifier.addListener(listener); timer = Timer(timeout, () { if (!completer.isCompleted) { debugPrint( '[remote_controller] wait source ready timeout ' 'reason=$reason ' 'state=${source.state}', ); completer.complete(); } }); try { listener(); await completer.future; } finally { timer.cancel(); source.stateNotifier.removeListener(listener); } debugPrint( '[remote_controller] source ready wait done ' 'reason=$reason ' 'state=${source.state}', ); } // ------------------------------------------------------------ // bootstrap flag // ------------------------------------------------------------ Future bootstrapDone() async { const storage = FlutterSecureStorage(); return (await storage.read(key: _kBootstrapDone)) == '1'; } Future _setBootstrapDone() async { const storage = FlutterSecureStorage(); await storage.write(key: _kBootstrapDone, value: '1'); } // ------------------------------------------------------------ // init bus (usata da HomePage init) // ------------------------------------------------------------ Future initBusFromSettings() async { final s = await RemoteSettings.load(); if (!s.enabled) { RemoteSyncBus.instance.setDisabled(); return; } RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.syncing; RemoteSyncBus.instance.progressNotifier.value = const RemoteSyncProgress( done: 0, total: 0, showOverlay: false, ); } // ------------------------------------------------------------ // ws url // ------------------------------------------------------------ String _deriveWsUrl(String baseUrl) { final uri = Uri.parse(baseUrl); final scheme = uri.scheme == 'http' ? 'ws' : 'wss'; final parts = uri.host.split('.'); if (parts.isEmpty) return '$scheme://${uri.host}'; parts[0] = '${parts[0]}-ws'; return '$scheme://${parts.join('.')}'; } String _resolveWsUrl(RemoteSettings s) => s.wsUrl.trim().isNotEmpty ? s.wsUrl.trim() : _deriveWsUrl(s.baseUrl); // ------------------------------------------------------------ // ensure stack single-flight // ------------------------------------------------------------ Future _ensureRealtimeStack(CollectionSource source) { final existing = _ensureStackFuture; if (existing != null) return existing; final fut = _ensureRealtimeStackImpl(source); _ensureStackFuture = fut; return fut.whenComplete(() { if (identical(_ensureStackFuture, fut)) _ensureStackFuture = null; }); } Future _ensureRealtimeStackImpl(CollectionSource source) async { final s = await RemoteSettings.load(); _stateStore ??= RemoteStateStore(); _deduper ??= EventRingDeduper(_stateStore!); final wsUrl = _resolveWsUrl(s); final rebuildAuth = _auth == null || _auth!.email != s.email || _auth!.password != s.password || _auth!.base.toString() != (s.baseUrl.endsWith('/') ? s.baseUrl : '${s.baseUrl}/'); final rebuildWs = _ws == null || _ws!.wsUrl != wsUrl; if (rebuildAuth || rebuildWs) { _auth = RemoteAuth(baseUrl: s.baseUrl, email: s.email, password: s.password); // allinea RemoteHttp (thumbs) allo stesso auth/token RemoteHttp.attach(baseUrl: s.baseUrl, auth: _auth!); await RemoteHttp.warmUp(); _api = RemoteHttpApi(baseUrl: s.baseUrl, auth: _auth!); _engine = RemoteSyncEngine( api: _api!, repo: RemoteRepository(localMediaDb.rawDb), source: source, state: _stateStore!, ); // chiudi eventuale ws precedente _ws?.close(); // ignore: unawaited_futures // crea la queue per batch di "added" con deduper e callback UI debounced final addedQueue = RemoteAddedQueue( api: _api!, engine: _engine!, deduper: _deduper!, onUiRefresh: () { unawaited(() async { await source.appendRemoteEntriesFromDb(); _notifyRemoteVisibilityChanged(source); }()); }, ); // dichiariamo la variabile prima per poterla referenziare nelle closure late RemoteWsClient wsClient; wsClient = RemoteWsClient( wsUrl: wsUrl, store: _stateStore!, deduper: _deduper!, // Chiamato solo dopo auth_ok, oppure dopo recovery_done. // Solo qui i remoti possono diventare visibili. onReady: () { debugPrint('[remote_controller] WS ready -> set upToDate'); RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.upToDate; RemoteSyncBus.instance.progressNotifier.value = null; _notifyRemoteVisibilityChanged(source); }, // Chiamato se il WS cade o non autentica. // In questo caso i remoti devono restare nascosti. onDisconnected: () { debugPrint('[remote_controller] WS disconnected -> serverDown / hide remotes'); final current = RemoteSyncBus.instance.stateNotifier.value; if (current != RemoteSyncState.disabled) { RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; RemoteSyncBus.instance.progressNotifier.value = null; source.invalidateEntries(); _notifyRemoteVisibilityChanged(source); } }, // Quando il server richiede recovery, esegui progressive sync // e poi invia recovery_done. RemoteWsClient chiamerà onReady() // dopo notifyRecoveryCompleted(). onNeedRecoverySync: () async { try { await _runProgressiveSync(source); } finally { wsClient.notifyRecoveryCompleted(); } }, // Delega gli altri eventi al controller. onEvent: (msg) => _handleWsEvent(source, msg), ); // registra la queue nel client WS wsClient.setAddedQueue(addedQueue); // salva la queue e il client nello stato del controller _addedQueue = addedQueue; _ws = wsClient; source.remoteWsClient = wsClient; // opzionale: inizializza la queue se ha init/async setup try { await _addedQueue?.init(); } catch (_) { // ignore init errors, queue funzionerà comunque } } else { await _deduper!.init(); } } // ------------------------------------------------------------ // progressive coalescing // ------------------------------------------------------------ void _requestProgressiveSync({ required CollectionSource source, String? sinceIso, Duration delay = const Duration(milliseconds: 600), }) { if (sinceIso != null && sinceIso.trim().isNotEmpty) { _pendingSinceIso = _minIso(_pendingSinceIso, sinceIso.trim()); } if (_progressiveScheduled) return; _progressiveScheduled = true; _progressiveTimer?.cancel(); _progressiveTimer = Timer(delay, () async { _progressiveScheduled = false; final since = _pendingSinceIso; _pendingSinceIso = null; try { await _runProgressiveSync(source, since: since); } catch (_) {} }); } String? _minIso(String? a, String b) { if (a == null || a.isEmpty) return b; final da = DateTime.tryParse(a); final db = DateTime.tryParse(b); if (da == null) return b; if (db == null) return a; return db.isBefore(da) ? b : a; } // ------------------------------------------------------------ // PATCH 1: progressive NON deve far tornare arancione se eri già verde // ------------------------------------------------------------ Future _runProgressiveSync( CollectionSource source, { String? since, }) async { if (_progressiveInFlight) { debugPrint( '[remote] progressive sync skipped: already in flight ' 'since=$since', ); return; } _progressiveInFlight = true; try { await _ensureRealtimeStack(source); final engine = _engine; if (engine == null) return; final prev = RemoteSyncBus.instance.stateNotifier.value; // Se eri già verde, non far lampeggiare l'icona in arancione. final keepGreen = prev == RemoteSyncState.upToDate; int? opId; if (!keepGreen) { opId = RemoteSyncBus.instance.start( total: 0, showOverlay: false, ); _notifyRemoteVisibilityChanged(source); } try { if (since != null && since.trim().isNotEmpty) { await engine.progressiveSyncFrom(since.trim()); } else { await engine.progressiveSync(); } if (opId != null) { RemoteSyncBus.instance.finishUpToDate(opId: opId); } else { // Resta verde. RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.upToDate; RemoteSyncBus.instance.progressNotifier.value = null; } // Importante: // NON chiamare computeFolderMap qui. // Il sync engine / appendRemoteEntriesFromDb aggiorna già DB/source/folderMap // quando serve. Qui basta notificare la UI. _notifyRemoteVisibilityChanged(source); } catch (e, st) { debugPrint('[remote] progressive sync failed: $e\n$st'); if (opId != null) { RemoteSyncBus.instance.failServerDown(opId: opId); } else { RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; RemoteSyncBus.instance.progressNotifier.value = null; } _notifyRemoteVisibilityChanged(source); rethrow; } } finally { _progressiveInFlight = false; } } // ------------------------------------------------------------ // ws start/stop // ------------------------------------------------------------ Future _startWsAlreadyEnsured() async { if (_wsConnectInFlight) return; _wsConnectInFlight = true; try { final s = await RemoteSettings.load(); if (!s.enabled) return; if (_ws == null || _auth == null) return; if (_ws!.isConnected) return; final token = _auth!.token ?? await _auth!.login(); await _ws!.connect(token: token); } finally { _wsConnectInFlight = false; } } Future _stopWs(CollectionSource source) async { _progressiveTimer?.cancel(); _progressiveTimer = null; _progressiveScheduled = false; _pendingSinceIso = null; try { await _ws?.close(); } catch (_) {} try { await _addedQueue?.dispose(); } catch (_) {} _addedQueue = null; _ws = null; _engine = null; _api = null; _auth = null; _ensureStackFuture = null; _wsConnectInFlight = false; _notifyRemoteVisibilityChanged(source); } // ------------------------------------------------------------ // ws events // ------------------------------------------------------------ Future _handleWsEvent(CollectionSource source, Map msg) async { final type = msg['type']?.toString(); if (type == null) return true; // BULK add_dir / del_dir: se il server fornisce "since", esegui subito la sync mirata if ((type == 'add_dir' || type == 'del_dir') && msg['mode'] == 'bulk') { final since = msg['since']?.toString(); if (since != null && since.isNotEmpty) { try { await _runProgressiveSync(source, since: since); return true; } catch (_) { // se fallisce, segnala al caller che non è stato applicato return false; } } // se non c'è since, accetta l'evento e aspetta eventuale add_dir_done return true; } // add_dir_done / del_dir_done: server conferma fine bulk, esegui la sync mirata se presente if ((type == 'add_dir_done' || type == 'del_dir_done') && msg['mode'] == 'bulk') { final since = msg['since']?.toString(); try { await _runProgressiveSync(source, since: since); return true; } catch (_) { return false; } } // added / del / removed / updated // - per added: la RemoteAddedQueue è già registrata nel WS client e gestisce batching // - per del/removed/updated: chiediamo una progressive coalesced (puoi ottimizzare per id) if (type == 'added' || type == 'del' || type == 'removed' || type == 'updated') { _requestProgressiveSync(source: source); return true; } return true; } // ------------------------------------------------------------ // lifecycle // ------------------------------------------------------------ Future onAppStart({ required CollectionSource source, bool resumeBootstrapIfEnabled = true, }) async { final s = await RemoteSettings.load(); debugPrint('[remote-debug] onAppStart start settings.enabled=${s.enabled} bus=${RemoteSyncBus.instance.stateNotifier.value}'); if (!s.enabled) { debugPrint('[remote-debug] remote disabled in settings -> disabling bus and hiding remotes'); RemoteSyncBus.instance.setDisabled(); debugPrint( '[remote-debug] remote disabled: keeping remote entries in memory, hiding by remoteVisible', ); // Non rimuoviamo remoti dalla RAM. // Non facciamo computeFolderMap qui: addEntries/init lo fa già. source.invalidateEntries(); _notifyRemoteVisibilityChanged(source); await _stopWs(source); debugPrint( '[remote-debug] onAppStart end (remote disabled) ' 'bus=${RemoteSyncBus.instance.stateNotifier.value}', ); return; } await _waitSourceReady( source, reason: 'onAppStart-remote-enabled', ); /* final done = await bootstrapDone(); debugPrint('[remote-debug] bootstrapDone=$done'); */ var done = await bootstrapDone(); final cachedRemotes = await localMediaDb.loadEntries( origin: RemoteOrigin.value, ); final hasCachedRemotes = cachedRemotes.any((entry) => !entry.trashed); debugPrint( '[remote-debug] bootstrapDone=$done ' 'cachedRemotes=${cachedRemotes.length} ' 'hasCachedRemotes=$hasCachedRemotes', ); // Se il flag bootstrap non esiste ma abbiamo già remoti nel DB, // consideriamo il bootstrap già fatto. // Questo evita fullSync/bootstrap inutili quando riattivi remote. if (!done && hasCachedRemotes) { debugPrint( '[remote-debug] bootstrap flag missing but remote cache exists ' '-> mark bootstrap done and skip full bootstrap', ); await _setBootstrapDone(); done = true; } if (!done) { debugPrint( '[remote_controller] first bootstrap start ' 'bus=${RemoteSyncBus.instance.stateNotifier.value}', ); RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.syncing; RemoteSyncBus.instance.progressNotifier.value = const RemoteSyncProgress( done: 0, total: 0, showOverlay: false, ); _notifyRemoteVisibilityChanged(source); debugPrint( '[remote_controller] first bootstrap set syncing ' 'bus=${RemoteSyncBus.instance.stateNotifier.value}', ); try { debugPrint('[remote_controller] first bootstrap -> ensure stack'); await _ensureRealtimeStack(source); debugPrint('[remote_controller] first bootstrap -> fullSyncImpl'); await _engine!.fullSyncImpl(); try { await _setBootstrapDone(); debugPrint('[remote_controller] bootstrap complete: flag set'); } catch (e, st) { debugPrint('[remote_controller] _setBootstrapDone failed: $e\n$st'); } // Dopo il primo bootstrap, carica subito i remoti dal DB nella source. // debugPrint('[remote_controller] first bootstrap -> append remotes from DB'); // await source.appendRemoteEntriesFromDb(); // Aggiorna visibilità/filter senza forzare refresh pesante thumbnails. _notifyRemoteVisibilityChanged(source); // Avvia realtime WS. // NON impostare upToDate qui. // I remoti diventano visibili solo dopo auth_ok del WS, // tramite RemoteWsClient.onReady(). debugPrint('[remote_controller] first bootstrap -> start WS'); try { await _startWsAlreadyEnsured(); debugPrint( '[remote_controller] first bootstrap WS start requested; ' 'waiting auth_ok before upToDate', ); } catch (e, st) { debugPrint('[remote_controller] first bootstrap WS start failed: $e\n$st'); RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; RemoteSyncBus.instance.progressNotifier.value = null; source.invalidateEntries(); _notifyRemoteVisibilityChanged(source); rethrow; } debugPrint( '[remote_controller] first bootstrap end, waiting WS auth_ok ' 'bus=${RemoteSyncBus.instance.stateNotifier.value}', ); // NON partire subito con progressive sync dopo full bootstrap. // Il bootstrap ha appena allineato tutto. // La progressive partirà da WS/eventi o resume. } catch (e, st) { debugPrint('[remote_controller] first bootstrap failed: $e\n$st'); RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; RemoteSyncBus.instance.progressNotifier.value = null; _notifyRemoteVisibilityChanged(source); _forceCollectionRefresh(source); rethrow; } return; } // prima di append: syncing + refresh RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.syncing; RemoteSyncBus.instance.progressNotifier.value = const RemoteSyncProgress(done: 0, total: 0, showOverlay: false); _notifyRemoteVisibilityChanged(source); debugPrint('[remote-debug] set state=syncing before append'); // append remoti dal DB (batching nella collection) debugPrint('[remote-debug] calling appendRemoteEntriesFromDb()'); await source.appendRemoteEntriesFromDb(); debugPrint('[remote-debug] after append state=${RemoteSyncBus.instance.stateNotifier.value} progress=${RemoteSyncBus.instance.progressNotifier.value}'); // forza refresh Home (cache/lens) _notifyRemoteVisibilityChanged(source); debugPrint('[remote-debug] notified visibility after append'); // Avvia lo stack realtime. // Dopo appendRemoteEntriesFromDb(), i remoti sono in RAM/DB, // ma restano nascosti finché il WS non conferma auth_ok. debugPrint('[remote-debug] ensuring realtime stack'); await _ensureRealtimeStack(source); debugPrint('[remote-debug] ensureRealtimeStack done, starting WS'); try { await _startWsAlreadyEnsured(); debugPrint( '[remote-debug] WS start requested ' 'ws client present=${_ws != null} ' 'wsConnected=${_ws?.isConnected}', ); // IMPORTANTE: // NON impostare upToDate qui. // _startWsAlreadyEnsured() avvia la connessione e manda auth, // ma auth_ok arriverà asincrono dallo stream WS. // // RemoteSyncState.upToDate verrà impostato solo da: // RemoteWsClient.onReady() debugPrint('[remote-debug] waiting WS auth_ok before showing remotes'); } catch (e, st) { debugPrint('[remote-debug] WS start failed -> serverDown: $e\n$st'); RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; RemoteSyncBus.instance.progressNotifier.value = null; source.invalidateEntries(); _notifyRemoteVisibilityChanged(source); return; } debugPrint('[remote-debug] exiting onAppStart without immediate progressive sync'); } Future toggleRemote({required CollectionSource source}) async { final s = await RemoteSettings.load(); debugPrint( '[remote_controller] toggleRemote start ' 'enabledBefore=${s.enabled} ' 'busBefore=${RemoteSyncBus.instance.stateNotifier.value}', ); final upd = RemoteSettings( enabled: !s.enabled, baseUrl: s.baseUrl, indexPath: s.indexPath, email: s.email, password: s.password, wsUrl: s.wsUrl, ); await upd.save(); debugPrint( '[remote_controller] toggleRemote saved ' 'enabledAfter=${upd.enabled} ' 'bus=${RemoteSyncBus.instance.stateNotifier.value}', ); if (!upd.enabled) { debugPrint( '[remote_controller] disabling remote ' 'busBeforeDisable=${RemoteSyncBus.instance.stateNotifier.value}', ); RemoteSyncBus.instance.setDisabled(); debugPrint( '[remote_controller] disabled remote ' 'busAfterDisable=${RemoteSyncBus.instance.stateNotifier.value}', ); debugPrint( '[remote_controller] disabling remote: keeping remote entries in memory, hiding by remoteVisible', ); source.invalidateEntries(); //await source.computeFolderMap(reason: 'remote-disabled-toggle'); //await source.updateDirectories(); _notifyRemoteVisibilityChanged(source); await _stopWs(source); return; } // ------------------------------------------------------------ // PATCH 2: se è la prima volta, mostra overlay (barra progresso) al bootstrap // ------------------------------------------------------------ final first = !(await bootstrapDone()); debugPrint( '[remote_controller] toggleRemote enabling ' 'first=$first ' 'busBeforeOnAppStart=${RemoteSyncBus.instance.stateNotifier.value}', ); await onAppStart(source: source, resumeBootstrapIfEnabled: first); debugPrint( '[remote_controller] toggleRemote after onAppStart ' 'busAfterOnAppStart=${RemoteSyncBus.instance.stateNotifier.value}', ); } // ------------------------------------------------------------ // full sync legacy (progress + overlay) // ------------------------------------------------------------ Future fullSync({ required CollectionSource source, required bool showOverlay, bool markBootstrapDoneOnSuccess = false, }) async { if (_syncInFlight) return; _syncInFlight = true; final s = await RemoteSettings.load(); if (!s.enabled) { RemoteSyncBus.instance.setDisabled(); _notifyRemoteVisibilityChanged(source); _forceCollectionRefresh(source); _syncInFlight = false; return; } try { if (s.baseUrl.trim().isEmpty) { RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; RemoteSyncBus.instance.progressNotifier.value = null; _notifyRemoteVisibilityChanged(source); _forceCollectionRefresh(source); _syncInFlight = false; return; } RemoteAuth? auth; if (s.email.isNotEmpty && s.password.isNotEmpty) { auth = RemoteAuth(baseUrl: s.baseUrl, email: s.email, password: s.password); } final client = RemoteJsonClient(s.baseUrl, s.indexPath, auth: auth); await client.ping().timeout(const Duration(seconds: 3)); final items = await client.fetchAll().timeout(const Duration(seconds: 30)); final total = items.length; final opId = RemoteSyncBus.instance.start(total: total, showOverlay: showOverlay); _notifyRemoteVisibilityChanged(source); final repo = RemoteRepository(localMediaDb.rawDb); // Decide se è bootstrap (solo in quel caso deleteAllRemotes) final isBootstrap = !(await bootstrapDone()); final serverIds = items.map((e) => e.id).where((v) => v.isNotEmpty).toSet(); if (isBootstrap) { debugPrint('[remote-debug] fullSync bootstrap: deleteAllRemotes + upsert chunked'); await repo.deleteAllRemotes(); // upsert in chunk più piccoli per non bloccare il main thread const chunk = 40; int done = 0; for (var i = 0; i < total; i += chunk) { final end = (i + chunk < total) ? i + chunk : total; await repo.upsertAll(items.sublist(i, end), chunkSize: chunk); done = end; RemoteSyncBus.instance.update(opId: opId, done: done, total: total); } try { await _setBootstrapDone(); debugPrint('[remote-debug] bootstrap flag set after fullSync'); } catch (e, st) { debugPrint('[remote-debug] _setBootstrapDone failed: $e\n$st'); } } else { debugPrint('[remote-debug] fullSync incremental: upsert chunked + pruneMissingRemotes'); const chunk = 40; int done = 0; for (var i = 0; i < total; i += chunk) { final end = (i + chunk < total) ? i + chunk : total; await repo.upsertAll(items.sublist(i, end), chunkSize: chunk); done = end; RemoteSyncBus.instance.update(opId: opId, done: done, total: total); } await repo.pruneMissingRemotes(serverIds); } // append remoti dal DB (collection batching) await source.appendRemoteEntriesFromDb(); _notifyRemoteVisibilityChanged(source); // Non impostare upToDate qui. // Dopo il fullSync i dati sono nel DB/source, ma i remoti devono diventare // visibili solo dopo auth_ok del WebSocket, tramite RemoteWsClient.onReady(). RemoteSyncBus.instance.progressNotifier.value = null; _notifyRemoteVisibilityChanged(source); // Assicurati che lo stack realtime sia avviato e poi avvia WS. // onReady() imposterà upToDate dopo auth_ok. await _ensureRealtimeStack(source); try { await _startWsAlreadyEnsured(); debugPrint( '[remote] fullSync completed, WS start requested; ' 'waiting auth_ok before showing remotes', ); } catch (e, st) { debugPrint('[remote] fullSync WS start failed: $e\n$st'); RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; RemoteSyncBus.instance.progressNotifier.value = null; source.invalidateEntries(); _notifyRemoteVisibilityChanged(source); } } catch (e, st) { debugPrint('[remote] fullSync error: $e\n$st'); RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; RemoteSyncBus.instance.progressNotifier.value = null; _notifyRemoteVisibilityChanged(source); _forceCollectionRefresh(source); _retryStartTime ??= DateTime.now(); _scheduleRetry(source); } finally { _syncInFlight = false; } } // ------------------------------------------------------------ // retry // ------------------------------------------------------------ void _scheduleRetry(CollectionSource source) { _retryTimer?.cancel(); _retryTimer = Timer(const Duration(seconds: 30), () async { final s = await RemoteSettings.load(); if (!s.enabled) return; if (_retryStartTime != null) { final elapsed = DateTime.now().difference(_retryStartTime!); if (elapsed > const Duration(minutes: 5)) { debugPrint('[remote] retry timeout -> disattivo remote'); debugPrint('[remote] retry timeout: keeping remote entries in memory, hiding by remoteVisible'); source.invalidateEntries(); // await source.computeFolderMap(reason: 'remote-disabled-retry-timeout'); // await source.updateDirectories(); _notifyRemoteVisibilityChanged(source); final upd = RemoteSettings( enabled: false, baseUrl: s.baseUrl, indexPath: s.indexPath, email: s.email, password: s.password, wsUrl: s.wsUrl, ); await upd.save(); RemoteSyncBus.instance.setDisabled(); _notifyRemoteVisibilityChanged(source); //_forceCollectionRefresh(source); await _stopWs(source); return; } } debugPrint('[remote] retry ping…'); final auth = (s.email.isNotEmpty && s.password.isNotEmpty) ? RemoteAuth(baseUrl: s.baseUrl, email: s.email, password: s.password) : null; final retryClient = RemoteJsonClient(s.baseUrl, s.indexPath, auth: auth); try { await retryClient.ping().timeout(const Duration(seconds: 3)); debugPrint('[remote] retry OK -> riprendo sync'); _retryTimer = null; _retryStartTime = null; unawaited(fullSync(source: source, showOverlay: false)); } catch (_) { debugPrint('[remote] retry fallito'); _scheduleRetry(source); } }); } Future onResume(CollectionSource source) async { final s = await RemoteSettings.load(); if (!s.enabled) return; await _ensureRealtimeStack(source); try { await _startWsAlreadyEnsured(); debugPrint( '[remote] onResume WS start requested; ' 'waiting auth_ok before showing remotes', ); } catch (e, st) { debugPrint('[remote] onResume WS start failed: $e\n$st'); RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; RemoteSyncBus.instance.progressNotifier.value = null; source.invalidateEntries(); _notifyRemoteVisibilityChanged(source); } // Non schedulare progressive qui. // Se serve recovery, sarà il WS/server a chiederla con need_recovery. } }