initial commit

This commit is contained in:
2026-07-27 14:42:23 +02:00
commit 833c68a189
257 changed files with 17947 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../connections/connection_providers.dart';
import 'node_master_api_client.dart';
part 'api_client_provider.g.dart';
/// Rebuilt (not mutated) whenever the active connection changes, so every
/// watcher automatically rebuilds against the new host with no manual
/// invalidation elsewhere. Returns null when no connection is configured —
/// `app_router.dart` redirects to Settings in that case rather than every
/// screen needing its own "no connection" branch.
@riverpod
NodeMasterApiClient? apiClient(Ref ref) {
final connection = ref.watch(activeConnectionProvider);
if (connection == null) return null;
final cancelToken = CancelToken();
ref.onDispose(() => cancelToken.cancel('Active connection changed.'));
final baseUrl = connection.baseUrl.endsWith('/')
? connection.baseUrl.substring(0, connection.baseUrl.length - 1)
: connection.baseUrl;
final dio = Dio(
BaseOptions(
baseUrl: '$baseUrl/v1',
connectTimeout: const Duration(seconds: 8),
sendTimeout: const Duration(seconds: 8),
receiveTimeout: const Duration(seconds: 15),
contentType: 'application/json',
),
);
if (kDebugMode) {
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
}
return NodeMasterApiClient(dio, cancelToken);
}
@@ -0,0 +1,73 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_client_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Rebuilt (not mutated) whenever the active connection changes, so every
/// watcher automatically rebuilds against the new host with no manual
/// invalidation elsewhere. Returns null when no connection is configured —
/// `app_router.dart` redirects to Settings in that case rather than every
/// screen needing its own "no connection" branch.
@ProviderFor(apiClient)
const apiClientProvider = ApiClientProvider._();
/// Rebuilt (not mutated) whenever the active connection changes, so every
/// watcher automatically rebuilds against the new host with no manual
/// invalidation elsewhere. Returns null when no connection is configured —
/// `app_router.dart` redirects to Settings in that case rather than every
/// screen needing its own "no connection" branch.
final class ApiClientProvider
extends
$FunctionalProvider<
NodeMasterApiClient?,
NodeMasterApiClient?,
NodeMasterApiClient?
>
with $Provider<NodeMasterApiClient?> {
/// Rebuilt (not mutated) whenever the active connection changes, so every
/// watcher automatically rebuilds against the new host with no manual
/// invalidation elsewhere. Returns null when no connection is configured —
/// `app_router.dart` redirects to Settings in that case rather than every
/// screen needing its own "no connection" branch.
const ApiClientProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'apiClientProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$apiClientHash();
@$internal
@override
$ProviderElement<NodeMasterApiClient?> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
NodeMasterApiClient? create(Ref ref) {
return apiClient(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(NodeMasterApiClient? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<NodeMasterApiClient?>(value),
);
}
}
String _$apiClientHash() => r'0a33d0449a51fd825f43bdcddd686bd413ff74ea';
+44
View File
@@ -0,0 +1,44 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'api_exception.freezed.dart';
/// Typed errors thrown by [NodeMasterApiClient]/repositories. Riverpod's
/// `AsyncValue` (via `FutureProvider`/`AsyncNotifier`) already catches
/// thrown exceptions into `AsyncValue.error`, so this is the result-type
/// boundary — no separate `Result<T, E>` wrapper is layered on top.
@freezed
sealed class ApiException with _$ApiException implements Exception {
/// DNS/connection failure — couldn't reach the host at all.
const factory ApiException.network(String message) = ApiNetworkException;
/// Connect/send/receive timeout.
const factory ApiException.timeout() = ApiTimeoutException;
/// The server responded with a 4xx/5xx and (usually) a `{"error": msg}`
/// body.
const factory ApiException.server(int statusCode, String message) = ApiServerException;
/// The response body wasn't the JSON shape a model expected.
const factory ApiException.parse(String message) = ApiParseException;
/// Request was cancelled (e.g. the active connection changed mid-flight).
const factory ApiException.cancelled() = ApiCancelledException;
/// Anything else.
const factory ApiException.unknown(String message) = ApiUnknownException;
}
extension ApiExceptionUserMessage on ApiException {
/// Short, user-facing copy — screens can use this directly or switch on
/// the exception type themselves for more specific handling.
String get userMessage => switch (this) {
ApiNetworkException() => "Can't reach this node — check the URL and that it's reachable.",
ApiTimeoutException() => 'The node took too long to respond.',
ApiServerException(:final statusCode, :final message) => statusCode == 404
? 'Not found.'
: 'Server error ($statusCode): $message',
ApiParseException() => 'Received an unexpected response from the node.',
ApiCancelledException() => 'Cancelled.',
ApiUnknownException(:final message) => message,
};
}
+528
View File
@@ -0,0 +1,528 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'api_exception.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ApiException {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiException);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'ApiException()';
}
}
/// @nodoc
class $ApiExceptionCopyWith<$Res> {
$ApiExceptionCopyWith(ApiException _, $Res Function(ApiException) __);
}
/// Adds pattern-matching-related methods to [ApiException].
extension ApiExceptionPatterns on ApiException {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( ApiNetworkException value)? network,TResult Function( ApiTimeoutException value)? timeout,TResult Function( ApiServerException value)? server,TResult Function( ApiParseException value)? parse,TResult Function( ApiCancelledException value)? cancelled,TResult Function( ApiUnknownException value)? unknown,required TResult orElse(),}){
final _that = this;
switch (_that) {
case ApiNetworkException() when network != null:
return network(_that);case ApiTimeoutException() when timeout != null:
return timeout(_that);case ApiServerException() when server != null:
return server(_that);case ApiParseException() when parse != null:
return parse(_that);case ApiCancelledException() when cancelled != null:
return cancelled(_that);case ApiUnknownException() when unknown != null:
return unknown(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( ApiNetworkException value) network,required TResult Function( ApiTimeoutException value) timeout,required TResult Function( ApiServerException value) server,required TResult Function( ApiParseException value) parse,required TResult Function( ApiCancelledException value) cancelled,required TResult Function( ApiUnknownException value) unknown,}){
final _that = this;
switch (_that) {
case ApiNetworkException():
return network(_that);case ApiTimeoutException():
return timeout(_that);case ApiServerException():
return server(_that);case ApiParseException():
return parse(_that);case ApiCancelledException():
return cancelled(_that);case ApiUnknownException():
return unknown(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( ApiNetworkException value)? network,TResult? Function( ApiTimeoutException value)? timeout,TResult? Function( ApiServerException value)? server,TResult? Function( ApiParseException value)? parse,TResult? Function( ApiCancelledException value)? cancelled,TResult? Function( ApiUnknownException value)? unknown,}){
final _that = this;
switch (_that) {
case ApiNetworkException() when network != null:
return network(_that);case ApiTimeoutException() when timeout != null:
return timeout(_that);case ApiServerException() when server != null:
return server(_that);case ApiParseException() when parse != null:
return parse(_that);case ApiCancelledException() when cancelled != null:
return cancelled(_that);case ApiUnknownException() when unknown != null:
return unknown(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String message)? network,TResult Function()? timeout,TResult Function( int statusCode, String message)? server,TResult Function( String message)? parse,TResult Function()? cancelled,TResult Function( String message)? unknown,required TResult orElse(),}) {final _that = this;
switch (_that) {
case ApiNetworkException() when network != null:
return network(_that.message);case ApiTimeoutException() when timeout != null:
return timeout();case ApiServerException() when server != null:
return server(_that.statusCode,_that.message);case ApiParseException() when parse != null:
return parse(_that.message);case ApiCancelledException() when cancelled != null:
return cancelled();case ApiUnknownException() when unknown != null:
return unknown(_that.message);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String message) network,required TResult Function() timeout,required TResult Function( int statusCode, String message) server,required TResult Function( String message) parse,required TResult Function() cancelled,required TResult Function( String message) unknown,}) {final _that = this;
switch (_that) {
case ApiNetworkException():
return network(_that.message);case ApiTimeoutException():
return timeout();case ApiServerException():
return server(_that.statusCode,_that.message);case ApiParseException():
return parse(_that.message);case ApiCancelledException():
return cancelled();case ApiUnknownException():
return unknown(_that.message);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String message)? network,TResult? Function()? timeout,TResult? Function( int statusCode, String message)? server,TResult? Function( String message)? parse,TResult? Function()? cancelled,TResult? Function( String message)? unknown,}) {final _that = this;
switch (_that) {
case ApiNetworkException() when network != null:
return network(_that.message);case ApiTimeoutException() when timeout != null:
return timeout();case ApiServerException() when server != null:
return server(_that.statusCode,_that.message);case ApiParseException() when parse != null:
return parse(_that.message);case ApiCancelledException() when cancelled != null:
return cancelled();case ApiUnknownException() when unknown != null:
return unknown(_that.message);case _:
return null;
}
}
}
/// @nodoc
class ApiNetworkException implements ApiException {
const ApiNetworkException(this.message);
final String message;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ApiNetworkExceptionCopyWith<ApiNetworkException> get copyWith => _$ApiNetworkExceptionCopyWithImpl<ApiNetworkException>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiNetworkException&&(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType,message);
@override
String toString() {
return 'ApiException.network(message: $message)';
}
}
/// @nodoc
abstract mixin class $ApiNetworkExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
factory $ApiNetworkExceptionCopyWith(ApiNetworkException value, $Res Function(ApiNetworkException) _then) = _$ApiNetworkExceptionCopyWithImpl;
@useResult
$Res call({
String message
});
}
/// @nodoc
class _$ApiNetworkExceptionCopyWithImpl<$Res>
implements $ApiNetworkExceptionCopyWith<$Res> {
_$ApiNetworkExceptionCopyWithImpl(this._self, this._then);
final ApiNetworkException _self;
final $Res Function(ApiNetworkException) _then;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
return _then(ApiNetworkException(
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class ApiTimeoutException implements ApiException {
const ApiTimeoutException();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiTimeoutException);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'ApiException.timeout()';
}
}
/// @nodoc
class ApiServerException implements ApiException {
const ApiServerException(this.statusCode, this.message);
final int statusCode;
final String message;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ApiServerExceptionCopyWith<ApiServerException> get copyWith => _$ApiServerExceptionCopyWithImpl<ApiServerException>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiServerException&&(identical(other.statusCode, statusCode) || other.statusCode == statusCode)&&(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType,statusCode,message);
@override
String toString() {
return 'ApiException.server(statusCode: $statusCode, message: $message)';
}
}
/// @nodoc
abstract mixin class $ApiServerExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
factory $ApiServerExceptionCopyWith(ApiServerException value, $Res Function(ApiServerException) _then) = _$ApiServerExceptionCopyWithImpl;
@useResult
$Res call({
int statusCode, String message
});
}
/// @nodoc
class _$ApiServerExceptionCopyWithImpl<$Res>
implements $ApiServerExceptionCopyWith<$Res> {
_$ApiServerExceptionCopyWithImpl(this._self, this._then);
final ApiServerException _self;
final $Res Function(ApiServerException) _then;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? statusCode = null,Object? message = null,}) {
return _then(ApiServerException(
null == statusCode ? _self.statusCode : statusCode // ignore: cast_nullable_to_non_nullable
as int,null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class ApiParseException implements ApiException {
const ApiParseException(this.message);
final String message;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ApiParseExceptionCopyWith<ApiParseException> get copyWith => _$ApiParseExceptionCopyWithImpl<ApiParseException>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiParseException&&(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType,message);
@override
String toString() {
return 'ApiException.parse(message: $message)';
}
}
/// @nodoc
abstract mixin class $ApiParseExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
factory $ApiParseExceptionCopyWith(ApiParseException value, $Res Function(ApiParseException) _then) = _$ApiParseExceptionCopyWithImpl;
@useResult
$Res call({
String message
});
}
/// @nodoc
class _$ApiParseExceptionCopyWithImpl<$Res>
implements $ApiParseExceptionCopyWith<$Res> {
_$ApiParseExceptionCopyWithImpl(this._self, this._then);
final ApiParseException _self;
final $Res Function(ApiParseException) _then;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
return _then(ApiParseException(
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class ApiCancelledException implements ApiException {
const ApiCancelledException();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiCancelledException);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'ApiException.cancelled()';
}
}
/// @nodoc
class ApiUnknownException implements ApiException {
const ApiUnknownException(this.message);
final String message;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ApiUnknownExceptionCopyWith<ApiUnknownException> get copyWith => _$ApiUnknownExceptionCopyWithImpl<ApiUnknownException>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiUnknownException&&(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType,message);
@override
String toString() {
return 'ApiException.unknown(message: $message)';
}
}
/// @nodoc
abstract mixin class $ApiUnknownExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
factory $ApiUnknownExceptionCopyWith(ApiUnknownException value, $Res Function(ApiUnknownException) _then) = _$ApiUnknownExceptionCopyWithImpl;
@useResult
$Res call({
String message
});
}
/// @nodoc
class _$ApiUnknownExceptionCopyWithImpl<$Res>
implements $ApiUnknownExceptionCopyWith<$Res> {
_$ApiUnknownExceptionCopyWithImpl(this._self, this._then);
final ApiUnknownException _self;
final $Res Function(ApiUnknownException) _then;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
return _then(ApiUnknownException(
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on
+39
View File
@@ -0,0 +1,39 @@
import 'package:dio/dio.dart';
import 'api_exception.dart';
/// Maps a caught [DioException] (or any other error) to a typed
/// [ApiException]. The one place this translation happens — repositories
/// and the API client both funnel through this rather than inspecting
/// `DioException` directly.
ApiException mapDioException(Object error) {
if (error is ApiException) return error;
if (error is! DioException) {
return ApiException.unknown(error.toString());
}
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
case DioExceptionType.transformTimeout:
return const ApiException.timeout();
case DioExceptionType.connectionError:
return ApiException.network(error.message ?? 'Connection failed.');
case DioExceptionType.cancel:
return const ApiException.cancelled();
case DioExceptionType.badResponse:
final response = error.response;
final statusCode = response?.statusCode ?? 0;
final data = response?.data;
final message = data is Map && data['error'] is String
? data['error'] as String
: (response?.statusMessage ?? 'Request failed.');
return ApiException.server(statusCode, message);
case DioExceptionType.badCertificate:
return ApiException.network('Bad certificate: ${error.message ?? ''}');
case DioExceptionType.unknown:
return ApiException.unknown(error.message ?? error.toString());
}
}
@@ -0,0 +1,261 @@
import 'dart:async';
import 'package:dio/dio.dart';
import '../models/models.dart';
import 'api_exception.dart';
import 'dio_error_mapper.dart';
/// Thin typed wrapper over [Dio], one method per NodeMaster REST endpoint.
/// Every method funnels errors through [mapDioException] so callers only
/// ever see [ApiException]. Holds a [CancelToken] tied to the client's own
/// lifetime (cancelled by [apiClientProvider] when the active connection
/// changes), so a request from an abandoned connection can't resolve into
/// the next connection's UI state.
class NodeMasterApiClient {
NodeMasterApiClient(this._dio, this._cancelToken);
final Dio _dio;
final CancelToken _cancelToken;
/// The overall budget for one request, enforced by Dart's own
/// `Future.timeout` rather than relying solely on Dio's
/// `connectTimeout`/`receiveTimeout` `BaseOptions`. Dio's browser HTTP
/// adapter has no socket-level hooks to honor those (the underlying
/// `fetch`/`XMLHttpRequest` APIs don't expose a connect-phase timeout),
/// so on web a request to an unreachable host would otherwise hang
/// until the browser's own (much longer, sometimes unbounded) default —
/// this timeout is what actually bounds it on every platform.
static const _requestBudget = Duration(seconds: 20);
Future<T> _guard<T>(Future<T> Function() body) async {
try {
return await body().timeout(_requestBudget);
} on ApiException {
rethrow;
} on DioException catch (e) {
throw mapDioException(e);
} on TimeoutException {
throw const ApiException.timeout();
} on TypeError catch (e) {
throw ApiException.parse(e.toString());
} on FormatException catch (e) {
throw ApiException.parse(e.toString());
}
}
Future<Response<dynamic>> _get(String path, {Map<String, dynamic>? query}) => _dio.get(
path,
queryParameters: query,
cancelToken: _cancelToken,
);
Future<Response<dynamic>> _post(String path, {Object? data}) => _dio.post(
path,
data: data,
cancelToken: _cancelToken,
);
Future<Response<dynamic>> _put(String path, {Object? data}) => _dio.put(
path,
data: data,
cancelToken: _cancelToken,
);
Future<Response<dynamic>> _delete(String path) => _dio.delete(path, cancelToken: _cancelToken);
// ---- node ----
Future<NodeInfo> getNode() => _guard(() async {
final res = await _get('/node/');
return NodeInfo.fromJson(res.data as Map<String, dynamic>);
});
Future<void> updateNode(NodeInfo node) => _guard(() async {
await _put('/node/', data: node.toJson());
});
Future<BackupConfig> getNodeBackup() => _guard(() async {
final res = await _get('/node/backup');
return BackupConfig.fromJson(res.data as Map<String, dynamic>);
});
Future<void> updateNodeBackup(BackupConfig config) => _guard(() async {
await _put('/node/backup', data: config.toJson());
});
Future<void> runNodeBackup() => _guard(() async {
await _post('/node/backup/run');
});
Future<List<BackupTarget>> getNodeBackupTargets() => _guard(() async {
final res = await _get('/node/backup/targets');
return (res.data as List<dynamic>)
.map((e) => BackupTarget.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<BackupTarget> addNodeBackupTarget(BackupTarget target) => _guard(() async {
final res = await _post('/node/backup/targets', data: target.toJson());
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
});
Future<BackupTarget> updateNodeBackupTarget(String targetId, BackupTarget target) => _guard(() async {
final res = await _put('/node/backup/targets/$targetId', data: target.toJson());
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
});
Future<void> deleteNodeBackupTarget(String targetId) => _guard(() async {
await _delete('/node/backup/targets/$targetId');
});
Future<void> runNodeBackupTarget(String targetId) => _guard(() async {
await _post('/node/backup/targets/$targetId/run');
});
Future<RunState> getNodeBackupTargetProgress(String targetId) => _guard(() async {
final res = await _get('/node/backup/targets/$targetId/progress');
return RunState.fromJson(res.data as Map<String, dynamic>);
});
Future<List<RunState>> getNodeBackupProgress() => _guard(() async {
final res = await _get('/node/backup/progress');
return (res.data as List<dynamic>)
.map((e) => RunState.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<ScanResult> scan({String? folder}) => _guard(() async {
final res = await _post('/node/scan', data: folder == null ? {} : {'folder': folder});
return ScanResult.fromJson(res.data as Map<String, dynamic>);
});
Future<List<UpdateRecord>> getUpdates() => _guard(() async {
final res = await _get('/node/updates');
return (res.data as List<dynamic>)
.map((e) => UpdateRecord.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<UpdateRecord> addUpdate(UpdateRecord record) => _guard(() async {
final res = await _post('/node/updates', data: record.toJson());
return UpdateRecord.fromJson(res.data as Map<String, dynamic>);
});
Future<void> deleteUpdate(String id) => _guard(() async {
await _delete('/node/updates/$id');
});
// ---- nodes (fleet registry) ----
Future<List<RemoteNode>> getAllRemoteNodes() => _guard(() async {
final res = await _get('/nodes/');
return (res.data as List<dynamic>)
.map((e) => RemoteNode.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<RemoteNode> addRemoteNode(RemoteNode node) => _guard(() async {
final res = await _post('/nodes/', data: node.toJson());
return RemoteNode.fromJson(res.data as Map<String, dynamic>);
});
Future<RemoteNode> getRemoteNode(String id) => _guard(() async {
final res = await _get('/nodes/$id');
return RemoteNode.fromJson(res.data as Map<String, dynamic>);
});
Future<RemoteNode> updateRemoteNode(String id, RemoteNode node) => _guard(() async {
final res = await _put('/nodes/$id', data: node.toJson());
return RemoteNode.fromJson(res.data as Map<String, dynamic>);
});
Future<void> deleteRemoteNode(String id) => _guard(() async {
await _delete('/nodes/$id');
});
Future<List<AggregatedNodeStatus>> getAggregated() => _guard(() async {
final res = await _get('/nodes/aggregated');
return (res.data as List<dynamic>)
.map((e) => AggregatedNodeStatus.fromJson(e as Map<String, dynamic>))
.toList();
});
// ---- services ----
Future<List<Service>> getAllServices() => _guard(() async {
final res = await _get('/services/');
return (res.data as List<dynamic>)
.map((e) => Service.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<Service> addService(Service service) => _guard(() async {
final res = await _post('/services/', data: service.toJson());
return Service.fromJson(res.data as Map<String, dynamic>);
});
Future<Service> getService(String id) => _guard(() async {
final res = await _get('/services/$id');
return Service.fromJson(res.data as Map<String, dynamic>);
});
Future<Service> updateService(String id, Service service) => _guard(() async {
final res = await _put('/services/$id', data: service.toJson());
return Service.fromJson(res.data as Map<String, dynamic>);
});
Future<void> deleteService(String id) => _guard(() async {
await _delete('/services/$id');
});
Future<void> startService(String id) => _guard(() async {
await _post('/services/$id/start');
});
Future<void> stopService(String id) => _guard(() async {
await _post('/services/$id/stop');
});
Future<void> backupService(String id) => _guard(() async {
await _post('/services/$id/backup');
});
Future<List<BackupTarget>> getServiceBackupTargets(String id) => _guard(() async {
final res = await _get('/services/$id/backup/targets');
return (res.data as List<dynamic>)
.map((e) => BackupTarget.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<BackupTarget> addServiceBackupTarget(String id, BackupTarget target) => _guard(() async {
final res = await _post('/services/$id/backup/targets', data: target.toJson());
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
});
Future<BackupTarget> updateServiceBackupTarget(String id, String targetId, BackupTarget target) =>
_guard(() async {
final res = await _put('/services/$id/backup/targets/$targetId', data: target.toJson());
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
});
Future<void> deleteServiceBackupTarget(String id, String targetId) => _guard(() async {
await _delete('/services/$id/backup/targets/$targetId');
});
Future<void> runServiceBackupTarget(String id, String targetId) => _guard(() async {
await _post('/services/$id/backup/targets/$targetId/run');
});
Future<RunState> getServiceBackupTargetProgress(String id, String targetId) => _guard(() async {
final res = await _get('/services/$id/backup/targets/$targetId/progress');
return RunState.fromJson(res.data as Map<String, dynamic>);
});
Future<List<RunState>> getServiceBackupProgress(String id) => _guard(() async {
final res = await _get('/services/$id/backup/progress');
return (res.data as List<dynamic>)
.map((e) => RunState.fromJson(e as Map<String, dynamic>))
.toList();
});
}