Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .example.env
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ NEXT_PUBLIC_APP_NAME=GhostClass

# ⚠️ App version displayed in footer and health checks
# 🔨 Build-time (Infisical `/build-time` folder)
NEXT_PUBLIC_APP_VERSION=4.3.5
NEXT_PUBLIC_APP_VERSION=4.3.6

# ⚠️ Minimum supported app version required to bypass forced update
# 🚀 Runtime (Infisical `/runtime` folder → Server Env Var)
MIN_APP_VERSION=4.3.5
MIN_APP_VERSION=4.3.6

# ⚠️ Your production domain WITHOUT https://
# All URL-based variables are derived from this.
Expand Down
2 changes: 1 addition & 1 deletion mobile/lib/config/app_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class AppConfig {

/// Current application version (derived from Infisical compilation injection).
static String get appVersion =>
const String.fromEnvironment('APP_VERSION', defaultValue: '4.3.5');
const String.fromEnvironment('APP_VERSION', defaultValue: '4.3.6');

/// Commit SHA injected by CI for release builds.
static String get appCommitSha =>
Expand Down
43 changes: 28 additions & 15 deletions mobile/lib/logic/ezygo_batch_fetcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class EzygoBatchFetcher {
// Tracker for log throttling
static DateTime? _lastCircuitBreakerLog;

static int _generation = 0;

/// Executes an authenticated request with deduplication and caching.
///
/// [path] The full URL or relative path.
Expand All @@ -62,6 +64,7 @@ class EzygoBatchFetcher {
// We include method, path, token, and a hash of the body data to avoid collisions.
final dataKey = data != null ? json.encode(data) : '';
final cacheKey = '$method|$path|$token|$dataKey';
final startGeneration = _generation;

// 0. Security Barrier: If the backend connection is compromised, block immediately.
if (_isBackendUnauthorized()) {
Expand Down Expand Up @@ -169,19 +172,26 @@ class EzygoBatchFetcher {
final response = await requestFuture;

// 5. Cache the result
if (response.statusCode == 200) {
// Success cache (Longer)
_cache[cacheKey] = _CacheEntry(
response: response,
expiry: DateTime.now().add(_cacheTtl),
);
} else if (response.statusCode != null && response.statusCode! >= 500) {
// NEGATIVE CACHE (Circuit Breaker):
// Remember 5xx failures briefly to prevent Request Storms.
_setOutage(true);
_cache[cacheKey] = _CacheEntry(
response: response,
expiry: DateTime.now().add(const Duration(seconds: 15)),
if (_generation == startGeneration) {
if (response.statusCode == 200) {
// Success cache (Longer)
_cache[cacheKey] = _CacheEntry(
response: response,
expiry: DateTime.now().add(_cacheTtl),
);
} else if (response.statusCode != null &&
response.statusCode! >= 500) {
// NEGATIVE CACHE (Circuit Breaker):
// Remember 5xx failures briefly to prevent Request Storms.
_setOutage(true);
_cache[cacheKey] = _CacheEntry(
response: response,
expiry: DateTime.now().add(const Duration(seconds: 15)),
);
}
} else {
AppLogger.d(
'EzygoBatchFetcher: Discarded caching for $path due to cache clear during request.',
);
}

Expand All @@ -193,7 +203,7 @@ class EzygoBatchFetcher {
e.type == DioExceptionType.connectionError) {
_setOutage(true);
// Short error TTL for transient network issues to recover faster
if (e.response != null) {
if (e.response != null && _generation == startGeneration) {
_cache[cacheKey] = _CacheEntry(
response: e.response!,
expiry: DateTime.now().add(const Duration(seconds: 5)),
Expand Down Expand Up @@ -261,7 +271,10 @@ class EzygoBatchFetcher {
_cache.clear();
_inFlight.clear();
_setOutage(false);
AppLogger.i('EzygoBatchFetcher: Cache and Outage state cleared.');
_generation++;
AppLogger.i(
'EzygoBatchFetcher: Cache and Outage state cleared. Generation updated to $_generation.',
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion mobile/lib/models/attendance.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ enum AttendanceStatus {
present(110),
absent(111),
otherLeave(112),
dutyLeave(225)
dutyLeave(225),
;

const AttendanceStatus(this.code);
Expand Down
10 changes: 4 additions & 6 deletions mobile/lib/providers/academic_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,10 @@ final academicProvider =
class AcademicNotifier extends AsyncNotifier<AcademicState?> {
@override
FutureOr<AcademicState?> build() async {
// Academic state is pre-populated by AuthNotifier during startup
// (via _fetchAndSaveAcademicContext) before authProvider.future resolves.
// We gate on auth being available to avoid building while logged out.
final authAsync = ref.watch(authProvider);
if (!authAsync.hasValue || authAsync.value == null) return null;
// Read authProvider to see if we have a logged-in user.
// Do NOT watch it to avoid circular dependency loops with authProvider's initialization.
final auth = ref.read(authProvider).value;
if (auth == null) return null;

final storage = ref.read(secureStorageProvider);

Expand All @@ -97,7 +96,6 @@ class AcademicNotifier extends AsyncNotifier<AcademicState?> {
if (cached != null) return cached;

// 2. Fallback: User settings from profile sync
final auth = authAsync.value!;
final semester = auth.settings.semester;
final year = auth.settings.academicYear;

Expand Down
Loading
Loading