import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:tatlock_ui/core/api/api_client.dart'; import 'package:tatlock_ui/features/control_room/containers/data/datasources/containers_datasource.dart'; import 'package:tatlock_ui/features/control_room/containers/domain/entities/container.dart'; import 'package:tatlock_ui/features/control_room/stacks/domain/entities/stack.dart'; part 'stacks_datasource.g.dart'; /// Data source for stack operations. /// /// Stacks are derived from container labels (Docker Compose convention). /// The Core API provides endpoints for stack YAML and environment variables. class StacksDatasource { StacksDatasource(this._dio, this._containersDatasource); final Dio _dio; final ContainersDatasource _containersDatasource; /// Gets all stacks by aggregating container data. Future> getStacks() async { final containers = await _containersDatasource.getContainers(); final containerEntities = containers.map((c) => c.toEntity()).toList(); // Group containers by stack name final stackMap = >{}; for (final container in containerEntities) { final stackName = container.stackName; if (stackName != null) { stackMap.putIfAbsent(stackName, () => []).add(container); } } // Convert to Stack entities return stackMap.entries.map((entry) { final name = entry.key; final stackContainers = entry.value; final runningCount = stackContainers.where((c) => c.isRunning).length; return Stack( id: name, // Use name as ID for compose stacks name: name, type: StackType.compose, status: runningCount == stackContainers.length ? StackStatus.active : runningCount > 0 ? StackStatus.active : StackStatus.inactive, containerCount: stackContainers.length, runningCount: runningCount, ); }).toList() ..sort((a, b) => a.name.compareTo(b.name)); } /// Gets a single stack by ID. Future getStack(String id) async { final stacks = await getStacks(); return stacks.firstWhere( (s) => s.id == id, orElse: () => throw Exception('Stack not found: $id'), ); } /// Performs an action on all containers in a stack. Future stackAction(String stackId, String action) async { final containers = await _containersDatasource.getContainers(); final stackContainers = containers .map((c) => c.toEntity()) .where((c) => c.stackId == stackId) .toList(); for (final container in stackContainers) { await _containersDatasource.containerAction(container.fullId, action); } } /// Gets the compose YAML for a stack. Future getStackYaml(String stackId) async { final response = await _dio.get( '/infrastructure/stacks/$stackId/compose', ); return response.data ?? ''; } /// Updates the compose YAML for a stack. Future updateStackYaml(String stackId, String yaml) async { await _dio.put( '/infrastructure/stacks/$stackId/compose', data: yaml, options: Options(contentType: 'text/yaml'), ); } /// Gets the environment variables for a stack. Future> getStackEnvVars(String stackId) async { final response = await _dio.get>( '/infrastructure/stacks/$stackId/env', ); return response.data?.map((k, v) => MapEntry(k, v.toString())) ?? {}; } /// Updates the environment variables for a stack. Future updateStackEnvVars( String stackId, Map envVars, ) async { await _dio.put( '/infrastructure/stacks/$stackId/env', data: envVars, ); } /// Deploys/redeploys a stack with current configuration. Future deployStack(String stackId) async { await _dio.post('/infrastructure/stacks/$stackId/deploy'); } /// Rebuilds a stack (pulls fresh images and recreates containers). Future rebuildStack(String stackId) async { await _dio.post('/infrastructure/stacks/$stackId/rebuild'); } } /// Provides the stacks datasource. @riverpod StacksDatasource stacksDatasource(StacksDatasourceRef ref) { final dio = ref.watch(coreApiClientProvider); final containersDatasource = ref.watch(containersDatasourceProvider); return StacksDatasource(dio, containersDatasource); }