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
@@ -0,0 +1,79 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../core/models/models.dart';
import '../../core/network/api_client_provider.dart';
import '../../core/network/node_master_api_client.dart';
part 'node_repository.g.dart';
/// Wraps the `/node/*` endpoints — this host's own identity, backup config,
/// scan trigger, and update log.
class NodeRepository {
const NodeRepository(this._client);
final NodeMasterApiClient _client;
Future<NodeInfo> getNode() => _client.getNode();
Future<void> updateNode(NodeInfo node) => _client.updateNode(node);
Future<BackupConfig> getBackup() => _client.getNodeBackup();
Future<void> updateBackup(BackupConfig config) => _client.updateNodeBackup(config);
Future<void> runBackup() => _client.runNodeBackup();
Future<List<BackupTarget>> getBackupTargets() => _client.getNodeBackupTargets();
Future<BackupTarget> addBackupTarget(BackupTarget target) => _client.addNodeBackupTarget(target);
Future<BackupTarget> updateBackupTarget(String targetId, BackupTarget target) =>
_client.updateNodeBackupTarget(targetId, target);
Future<void> deleteBackupTarget(String targetId) => _client.deleteNodeBackupTarget(targetId);
Future<void> runBackupTarget(String targetId) => _client.runNodeBackupTarget(targetId);
Future<RunState> getBackupTargetProgress(String targetId) =>
_client.getNodeBackupTargetProgress(targetId);
Future<List<RunState>> getBackupProgress() => _client.getNodeBackupProgress();
Future<ScanResult> scan({String? folder}) => _client.scan(folder: folder);
Future<List<UpdateRecord>> getUpdates() => _client.getUpdates();
Future<UpdateRecord> addUpdate(UpdateRecord record) => _client.addUpdate(record);
Future<void> deleteUpdate(String id) => _client.deleteUpdate(id);
}
/// Null when no connection is active — mirrors [apiClientProvider] so every
/// downstream provider that watches this gets the same "no connection"
/// signal without re-deriving it from the connection state itself.
@riverpod
NodeRepository? nodeRepository(Ref ref) {
final client = ref.watch(apiClientProvider);
if (client == null) return null;
return NodeRepository(client);
}
/// The active connection's node info, or null if no connection is
/// configured. Exceptions from an unreachable/misbehaving node surface as
/// [AsyncError] — this is also the smoke-test path proving the client →
/// repository → provider chain works end to end against a live server.
@riverpod
Future<NodeInfo?> currentNode(Ref ref) async {
final repo = ref.watch(nodeRepositoryProvider);
if (repo == null) return null;
return repo.getNode();
}
/// The node-level backup config — manual-refresh only, same reasoning as
/// `servicesListProvider`: this backs a detail-heavy screen with its own
/// edit dialog, where a silent background refresh could yank state out
/// from under an in-progress edit.
@riverpod
Future<BackupConfig?> backupConfig(Ref ref) async {
final repo = ref.watch(nodeRepositoryProvider);
if (repo == null) return null;
return repo.getBackup();
}
/// The OS update log — manual-refresh only, same reasoning as the other
/// list providers in this app.
@riverpod
Future<List<UpdateRecord>> updatesList(Ref ref) async {
final repo = ref.watch(nodeRepositoryProvider);
if (repo == null) return [];
return repo.getUpdates();
}
@@ -0,0 +1,216 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'node_repository.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Null when no connection is active — mirrors [apiClientProvider] so every
/// downstream provider that watches this gets the same "no connection"
/// signal without re-deriving it from the connection state itself.
@ProviderFor(nodeRepository)
const nodeRepositoryProvider = NodeRepositoryProvider._();
/// Null when no connection is active — mirrors [apiClientProvider] so every
/// downstream provider that watches this gets the same "no connection"
/// signal without re-deriving it from the connection state itself.
final class NodeRepositoryProvider
extends
$FunctionalProvider<NodeRepository?, NodeRepository?, NodeRepository?>
with $Provider<NodeRepository?> {
/// Null when no connection is active — mirrors [apiClientProvider] so every
/// downstream provider that watches this gets the same "no connection"
/// signal without re-deriving it from the connection state itself.
const NodeRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'nodeRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$nodeRepositoryHash();
@$internal
@override
$ProviderElement<NodeRepository?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
NodeRepository? create(Ref ref) {
return nodeRepository(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(NodeRepository? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<NodeRepository?>(value),
);
}
}
String _$nodeRepositoryHash() => r'c7cf7aac4b407fd4a21a56f5c3d34c81fa9abe5e';
/// The active connection's node info, or null if no connection is
/// configured. Exceptions from an unreachable/misbehaving node surface as
/// [AsyncError] — this is also the smoke-test path proving the client →
/// repository → provider chain works end to end against a live server.
@ProviderFor(currentNode)
const currentNodeProvider = CurrentNodeProvider._();
/// The active connection's node info, or null if no connection is
/// configured. Exceptions from an unreachable/misbehaving node surface as
/// [AsyncError] — this is also the smoke-test path proving the client →
/// repository → provider chain works end to end against a live server.
final class CurrentNodeProvider
extends
$FunctionalProvider<
AsyncValue<NodeInfo?>,
NodeInfo?,
FutureOr<NodeInfo?>
>
with $FutureModifier<NodeInfo?>, $FutureProvider<NodeInfo?> {
/// The active connection's node info, or null if no connection is
/// configured. Exceptions from an unreachable/misbehaving node surface as
/// [AsyncError] — this is also the smoke-test path proving the client →
/// repository → provider chain works end to end against a live server.
const CurrentNodeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'currentNodeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$currentNodeHash();
@$internal
@override
$FutureProviderElement<NodeInfo?> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<NodeInfo?> create(Ref ref) {
return currentNode(ref);
}
}
String _$currentNodeHash() => r'e1742b62ba610695d0e8a560d554902fa29d128c';
/// The node-level backup config — manual-refresh only, same reasoning as
/// `servicesListProvider`: this backs a detail-heavy screen with its own
/// edit dialog, where a silent background refresh could yank state out
/// from under an in-progress edit.
@ProviderFor(backupConfig)
const backupConfigProvider = BackupConfigProvider._();
/// The node-level backup config — manual-refresh only, same reasoning as
/// `servicesListProvider`: this backs a detail-heavy screen with its own
/// edit dialog, where a silent background refresh could yank state out
/// from under an in-progress edit.
final class BackupConfigProvider
extends
$FunctionalProvider<
AsyncValue<BackupConfig?>,
BackupConfig?,
FutureOr<BackupConfig?>
>
with $FutureModifier<BackupConfig?>, $FutureProvider<BackupConfig?> {
/// The node-level backup config — manual-refresh only, same reasoning as
/// `servicesListProvider`: this backs a detail-heavy screen with its own
/// edit dialog, where a silent background refresh could yank state out
/// from under an in-progress edit.
const BackupConfigProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'backupConfigProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$backupConfigHash();
@$internal
@override
$FutureProviderElement<BackupConfig?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<BackupConfig?> create(Ref ref) {
return backupConfig(ref);
}
}
String _$backupConfigHash() => r'd3eeedd7536dd8610b34b922bd5658c507c91efe';
/// The OS update log — manual-refresh only, same reasoning as the other
/// list providers in this app.
@ProviderFor(updatesList)
const updatesListProvider = UpdatesListProvider._();
/// The OS update log — manual-refresh only, same reasoning as the other
/// list providers in this app.
final class UpdatesListProvider
extends
$FunctionalProvider<
AsyncValue<List<UpdateRecord>>,
List<UpdateRecord>,
FutureOr<List<UpdateRecord>>
>
with
$FutureModifier<List<UpdateRecord>>,
$FutureProvider<List<UpdateRecord>> {
/// The OS update log — manual-refresh only, same reasoning as the other
/// list providers in this app.
const UpdatesListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'updatesListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$updatesListHash();
@$internal
@override
$FutureProviderElement<List<UpdateRecord>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<UpdateRecord>> create(Ref ref) {
return updatesList(ref);
}
}
String _$updatesListHash() => r'01c6dd3b69dfd8f98e57f29e43e3fa60d014a6e4';
@@ -0,0 +1,60 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../core/models/models.dart';
import '../../core/network/api_client_provider.dart';
import '../../core/network/node_master_api_client.dart';
import '../../core/polling/polling_async_notifier.dart';
part 'nodes_repository.g.dart';
/// Wraps the `/nodes/*` endpoints — the registry of sibling NodeMaster
/// hosts and the server-side fan-out status view.
class NodesRepository {
const NodesRepository(this._client);
final NodeMasterApiClient _client;
Future<List<RemoteNode>> getAll() => _client.getAllRemoteNodes();
Future<RemoteNode> get(String id) => _client.getRemoteNode(id);
Future<RemoteNode> add(RemoteNode node) => _client.addRemoteNode(node);
Future<RemoteNode> update(String id, RemoteNode node) => _client.updateRemoteNode(id, node);
Future<void> delete(String id) => _client.deleteRemoteNode(id);
Future<List<AggregatedNodeStatus>> getAggregated() => _client.getAggregated();
}
/// Null when no connection is active, mirroring [apiClientProvider].
@riverpod
NodesRepository? nodesRepository(Ref ref) {
final client = ref.watch(apiClientProvider);
if (client == null) return null;
return NodesRepository(client);
}
/// The registered-nodes list. Manual-refresh only, same reasoning as
/// `servicesListProvider` — this backs Fleet's registry table (M6).
@riverpod
Future<List<RemoteNode>> remoteNodesList(Ref ref) async {
final repo = ref.watch(nodesRepositoryProvider);
if (repo == null) return [];
return repo.getAll();
}
/// The live `/nodes/aggregated` fan-out result, polled every 25s. Shared
/// by the Dashboard's "Fleet" stat tile and the Fleet screen's card grid
/// (M6) — one poll, cached by Riverpod, rather than each screen running
/// its own duplicate timer against the same endpoint.
@riverpod
class AggregatedNodes extends _$AggregatedNodes with PollingMixin<List<AggregatedNodeStatus>> {
@override
Duration get interval => const Duration(seconds: 25);
@override
Future<List<AggregatedNodeStatus>> fetch() async {
final repo = ref.watch(nodesRepositoryProvider);
if (repo == null) return [];
return repo.getAggregated();
}
@override
Future<List<AggregatedNodeStatus>> build() => startPolling();
}
@@ -0,0 +1,180 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'nodes_repository.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Null when no connection is active, mirroring [apiClientProvider].
@ProviderFor(nodesRepository)
const nodesRepositoryProvider = NodesRepositoryProvider._();
/// Null when no connection is active, mirroring [apiClientProvider].
final class NodesRepositoryProvider
extends
$FunctionalProvider<
NodesRepository?,
NodesRepository?,
NodesRepository?
>
with $Provider<NodesRepository?> {
/// Null when no connection is active, mirroring [apiClientProvider].
const NodesRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'nodesRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$nodesRepositoryHash();
@$internal
@override
$ProviderElement<NodesRepository?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
NodesRepository? create(Ref ref) {
return nodesRepository(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(NodesRepository? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<NodesRepository?>(value),
);
}
}
String _$nodesRepositoryHash() => r'7dd03a46ea79ae87bb251ff273ff77a634a53f5f';
/// The registered-nodes list. Manual-refresh only, same reasoning as
/// `servicesListProvider` — this backs Fleet's registry table (M6).
@ProviderFor(remoteNodesList)
const remoteNodesListProvider = RemoteNodesListProvider._();
/// The registered-nodes list. Manual-refresh only, same reasoning as
/// `servicesListProvider` — this backs Fleet's registry table (M6).
final class RemoteNodesListProvider
extends
$FunctionalProvider<
AsyncValue<List<RemoteNode>>,
List<RemoteNode>,
FutureOr<List<RemoteNode>>
>
with $FutureModifier<List<RemoteNode>>, $FutureProvider<List<RemoteNode>> {
/// The registered-nodes list. Manual-refresh only, same reasoning as
/// `servicesListProvider` — this backs Fleet's registry table (M6).
const RemoteNodesListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'remoteNodesListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$remoteNodesListHash();
@$internal
@override
$FutureProviderElement<List<RemoteNode>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<RemoteNode>> create(Ref ref) {
return remoteNodesList(ref);
}
}
String _$remoteNodesListHash() => r'a2d3bd0b2e2f2cd54a7590bb5ace8a0c63e3d2aa';
/// The live `/nodes/aggregated` fan-out result, polled every 25s. Shared
/// by the Dashboard's "Fleet" stat tile and the Fleet screen's card grid
/// (M6) — one poll, cached by Riverpod, rather than each screen running
/// its own duplicate timer against the same endpoint.
@ProviderFor(AggregatedNodes)
const aggregatedNodesProvider = AggregatedNodesProvider._();
/// The live `/nodes/aggregated` fan-out result, polled every 25s. Shared
/// by the Dashboard's "Fleet" stat tile and the Fleet screen's card grid
/// (M6) — one poll, cached by Riverpod, rather than each screen running
/// its own duplicate timer against the same endpoint.
final class AggregatedNodesProvider
extends
$AsyncNotifierProvider<AggregatedNodes, List<AggregatedNodeStatus>> {
/// The live `/nodes/aggregated` fan-out result, polled every 25s. Shared
/// by the Dashboard's "Fleet" stat tile and the Fleet screen's card grid
/// (M6) — one poll, cached by Riverpod, rather than each screen running
/// its own duplicate timer against the same endpoint.
const AggregatedNodesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'aggregatedNodesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$aggregatedNodesHash();
@$internal
@override
AggregatedNodes create() => AggregatedNodes();
}
String _$aggregatedNodesHash() => r'14dd7a1e7d4b9caf2d7c975613555977c037dceb';
/// The live `/nodes/aggregated` fan-out result, polled every 25s. Shared
/// by the Dashboard's "Fleet" stat tile and the Fleet screen's card grid
/// (M6) — one poll, cached by Riverpod, rather than each screen running
/// its own duplicate timer against the same endpoint.
abstract class _$AggregatedNodes
extends $AsyncNotifier<List<AggregatedNodeStatus>> {
FutureOr<List<AggregatedNodeStatus>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref
as $Ref<
AsyncValue<List<AggregatedNodeStatus>>,
List<AggregatedNodeStatus>
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<List<AggregatedNodeStatus>>,
List<AggregatedNodeStatus>
>,
AsyncValue<List<AggregatedNodeStatus>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@@ -0,0 +1,57 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../core/models/models.dart';
import '../../core/network/api_client_provider.dart';
import '../../core/network/node_master_api_client.dart';
part 'services_repository.g.dart';
/// Wraps the `/services/*` endpoints — the compose-based services on this
/// host and their start/stop/backup actions.
class ServicesRepository {
const ServicesRepository(this._client);
final NodeMasterApiClient _client;
Future<List<Service>> getAll() => _client.getAllServices();
Future<Service> get(String id) => _client.getService(id);
Future<Service> add(Service service) => _client.addService(service);
Future<Service> update(String id, Service service) => _client.updateService(id, service);
Future<void> delete(String id) => _client.deleteService(id);
Future<void> start(String id) => _client.startService(id);
Future<void> stop(String id) => _client.stopService(id);
Future<void> backupNow(String id) => _client.backupService(id);
Future<List<BackupTarget>> getBackupTargets(String id) => _client.getServiceBackupTargets(id);
Future<BackupTarget> addBackupTarget(String id, BackupTarget target) =>
_client.addServiceBackupTarget(id, target);
Future<BackupTarget> updateBackupTarget(String id, String targetId, BackupTarget target) =>
_client.updateServiceBackupTarget(id, targetId, target);
Future<void> deleteBackupTarget(String id, String targetId) =>
_client.deleteServiceBackupTarget(id, targetId);
Future<void> runBackupTarget(String id, String targetId) =>
_client.runServiceBackupTarget(id, targetId);
Future<RunState> getBackupTargetProgress(String id, String targetId) =>
_client.getServiceBackupTargetProgress(id, targetId);
Future<List<RunState>> getBackupProgress(String id) => _client.getServiceBackupProgress(id);
}
/// Null when no connection is active, mirroring [apiClientProvider].
@riverpod
ServicesRepository? servicesRepository(Ref ref) {
final client = ref.watch(apiClientProvider);
if (client == null) return null;
return ServicesRepository(client);
}
/// The full services list for the active connection. Manual-refresh only
/// (not polled) — this is a detail-heavy table screen where a mid-edit
/// slide-over silently refreshing underneath the user would be worse than
/// slightly stale data; Dashboard's polled summary is where "did something
/// change" gets noticed at a glance.
@riverpod
Future<List<Service>> servicesList(Ref ref) async {
final repo = ref.watch(servicesRepositoryProvider);
if (repo == null) return [];
return repo.getAll();
}
@@ -0,0 +1,118 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'services_repository.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Null when no connection is active, mirroring [apiClientProvider].
@ProviderFor(servicesRepository)
const servicesRepositoryProvider = ServicesRepositoryProvider._();
/// Null when no connection is active, mirroring [apiClientProvider].
final class ServicesRepositoryProvider
extends
$FunctionalProvider<
ServicesRepository?,
ServicesRepository?,
ServicesRepository?
>
with $Provider<ServicesRepository?> {
/// Null when no connection is active, mirroring [apiClientProvider].
const ServicesRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'servicesRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$servicesRepositoryHash();
@$internal
@override
$ProviderElement<ServicesRepository?> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ServicesRepository? create(Ref ref) {
return servicesRepository(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ServicesRepository? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ServicesRepository?>(value),
);
}
}
String _$servicesRepositoryHash() =>
r'6eab348f1911803394716fd8f90a079bfe5cbda1';
/// The full services list for the active connection. Manual-refresh only
/// (not polled) — this is a detail-heavy table screen where a mid-edit
/// slide-over silently refreshing underneath the user would be worse than
/// slightly stale data; Dashboard's polled summary is where "did something
/// change" gets noticed at a glance.
@ProviderFor(servicesList)
const servicesListProvider = ServicesListProvider._();
/// The full services list for the active connection. Manual-refresh only
/// (not polled) — this is a detail-heavy table screen where a mid-edit
/// slide-over silently refreshing underneath the user would be worse than
/// slightly stale data; Dashboard's polled summary is where "did something
/// change" gets noticed at a glance.
final class ServicesListProvider
extends
$FunctionalProvider<
AsyncValue<List<Service>>,
List<Service>,
FutureOr<List<Service>>
>
with $FutureModifier<List<Service>>, $FutureProvider<List<Service>> {
/// The full services list for the active connection. Manual-refresh only
/// (not polled) — this is a detail-heavy table screen where a mid-edit
/// slide-over silently refreshing underneath the user would be worse than
/// slightly stale data; Dashboard's polled summary is where "did something
/// change" gets noticed at a glance.
const ServicesListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'servicesListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$servicesListHash();
@$internal
@override
$FutureProviderElement<List<Service>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<Service>> create(Ref ref) {
return servicesList(ref);
}
}
String _$servicesListHash() => r'e9d080c9ce0c0796d39c663ba79d7b67a2cf3adc';