feat(app): offline-кэш GET-запросов на дашборде — фаза 5

CacheInterceptor кэширует каждый успешный GET по путь+параметры в drift
(sqlite нативно, wasm+OPFS в браузере) и подменяет им сетевую ошибку;
провайдер отдаёт Cached<T>, экран показывает баннер «данные на …».
Контракт для остальных экранов — docs/ai/offline-cache.md.

flake.nix: libsecret/pkg-config для линуксовой сборки, jq/curl для
just app-web-assets (сборка sqlite3.wasm + drift_worker.js).
This commit is contained in:
Dmitry
2026-09-19 12:40:08 +03:00
parent 95cd6f176e
commit ffc5ed959a
21 changed files with 15852 additions and 60 deletions
+36
View File
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import '../utils/ru_date.dart';
/// "Нет соединения — данные на …": shown instead of pretending a screen built
/// from the offline cache is current. Pass the oldest `fetchedAt` among the
/// providers a screen watches (`core/cache/cached.dart`'s `oldestFetch`).
class StaleBanner extends StatelessWidget {
const StaleBanner({required this.fetchedAt, super.key});
final DateTime fetchedAt;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final at = fetchedAt.toLocal();
final time = '${at.hour.toString().padLeft(2, '0')}:${at.minute.toString().padLeft(2, '0')}';
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(color: scheme.errorContainer, borderRadius: BorderRadius.circular(8)),
child: Row(
children: [
Icon(Icons.cloud_off, size: 18, color: scheme.onErrorContainer),
const SizedBox(width: 8),
Expanded(
child: Text(
'Нет соединения — данные на ${ruDate(at)} $time',
style: TextStyle(color: scheme.onErrorContainer),
),
),
],
),
);
}
}