migrated the breaker system to MarianumConnect and refactored the API response to a rule-based model with support for version-specific blocks using maxBuild.

This commit is contained in:
2026-07-06 21:15:39 +02:00
parent 38a271929c
commit 2be5051d12
13 changed files with 213 additions and 118 deletions
@@ -0,0 +1,26 @@
import 'package:dio/dio.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'get_breakers_response.dart';
/// Fetches the app maintenance breaker rules from `GET /api/mobile/v1/breaker`.
/// The endpoint is public: the bearer token is attached if present but not
/// required, so this also works before login (e.g. to block the whole app).
class GetBreakers {
final Dio _dio;
GetBreakers({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<GetBreakersResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('breaker'),
);
return GetBreakersResponse.fromJson(response.data!);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
}
@@ -0,0 +1,14 @@
import '../../../request_cache.dart';
import 'get_breakers.dart';
import 'get_breakers_response.dart';
class GetBreakersCache extends SimpleCache<GetBreakersResponse> {
GetBreakersCache({super.onUpdate, super.renew, super.onError})
: super(
cacheTime: RequestCache.cacheMinute,
loader: () => GetBreakers().run(),
fromJson: GetBreakersResponse.fromJson,
) {
start('breakers');
}
}
@@ -0,0 +1,70 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_response.dart';
part 'get_breakers_response.g.dart';
/// App maintenance breaker rules delivered by
/// `GET /api/mobile/v1/breaker`. Each rule blocks the listed [areas] with a
/// [message]; [BreakerRule.maxBuild] optionally scopes it to old app builds.
///
/// [rules] defaults to empty so a payload in the pre-migration shape
/// (`global`/`regional`) — which may still sit in the hydrated/cache store
/// after an update — parses to "nothing blocked" instead of throwing.
@JsonSerializable(explicitToJson: true)
class GetBreakersResponse extends ApiResponse {
@JsonKey(defaultValue: [])
List<BreakerRule> rules;
GetBreakersResponse(this.rules);
factory GetBreakersResponse.fromJson(Map<String, dynamic> json) =>
_$GetBreakersResponseFromJson(json);
Map<String, dynamic> toJson() => _$GetBreakersResponseToJson(this);
}
@JsonSerializable()
class BreakerRule {
List<BreakerArea> areas;
String message;
/// When set, the rule only applies to app builds whose build number is
/// &le; [maxBuild] (force-update of old versions). `null` = every build.
int? maxBuild;
BreakerRule(this.areas, this.message, this.maxBuild);
factory BreakerRule.fromJson(Map<String, dynamic> json) =>
_$BreakerRuleFromJson(json);
Map<String, dynamic> toJson() => _$BreakerRuleToJson(this);
}
/// Blockable areas of the app. Each app module maps itself to one of these via
/// its `breakerArea` (see `app_modules.dart`). Must stay in sync with the
/// backend enum `BreakerArea` in the MarianumConnect `marMobileApi` service —
/// the backend only offers these values in the admin UI, so when a new blockable
/// module is added here, add the matching value on the backend too.
enum BreakerArea {
@JsonValue('GLOBAL')
global,
@JsonValue('TIMETABLE')
timetable,
@JsonValue('TALK')
talk,
@JsonValue('FILES')
files,
@JsonValue('NEWS')
news,
@JsonValue('ROOMPLAN')
roomPlan,
@JsonValue('GRADES')
grades,
@JsonValue('HOLIDAYS')
holidays,
@JsonValue('DATES')
dates,
@JsonValue('FEEDBACK')
feedback,
@JsonValue('MORE')
more,
}
@@ -0,0 +1,54 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_breakers_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GetBreakersResponse _$GetBreakersResponseFromJson(Map<String, dynamic> json) =>
GetBreakersResponse(
(json['rules'] as List<dynamic>?)
?.map((e) => BreakerRule.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
)
..headers = (json['headers'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
);
Map<String, dynamic> _$GetBreakersResponseToJson(
GetBreakersResponse instance,
) => <String, dynamic>{
'headers': ?instance.headers,
'rules': instance.rules.map((e) => e.toJson()).toList(),
};
BreakerRule _$BreakerRuleFromJson(Map<String, dynamic> json) => BreakerRule(
(json['areas'] as List<dynamic>)
.map((e) => $enumDecode(_$BreakerAreaEnumMap, e))
.toList(),
json['message'] as String,
(json['maxBuild'] as num?)?.toInt(),
);
Map<String, dynamic> _$BreakerRuleToJson(BreakerRule instance) =>
<String, dynamic>{
'areas': instance.areas.map((e) => _$BreakerAreaEnumMap[e]!).toList(),
'message': instance.message,
'maxBuild': instance.maxBuild,
};
const _$BreakerAreaEnumMap = {
BreakerArea.global: 'GLOBAL',
BreakerArea.timetable: 'TIMETABLE',
BreakerArea.talk: 'TALK',
BreakerArea.files: 'FILES',
BreakerArea.news: 'NEWS',
BreakerArea.roomPlan: 'ROOMPLAN',
BreakerArea.grades: 'GRADES',
BreakerArea.holidays: 'HOLIDAYS',
BreakerArea.dates: 'DATES',
BreakerArea.feedback: 'FEEDBACK',
BreakerArea.more: 'MORE',
};