feat: add Control Room with containers and stacks management

- Add Control Room page with stacks sidebar and containers list
- Implement container actions (start/stop/restart) with snackbars
- Add container logs viewer dialog
- Add search/filter for both stacks and containers lists
- Add external links for Portainer and Netdata (url_launcher)
- Add VS Code launch configuration for Flutter web debugging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2025-12-31 02:01:01 +01:00
co-authored by Claude Opus 4.5
parent dd6bcbdbda
commit 3b89ed8c18
42 changed files with 4166 additions and 21 deletions
@@ -0,0 +1,84 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../domain/entities/container.dart';
import '../../domain/repositories/container_repository.dart';
import '../datasources/containers_datasource.dart';
part 'container_repository_impl.g.dart';
/// Implementation of ContainerRepository using remote datasource.
class ContainerRepositoryImpl implements ContainerRepository {
ContainerRepositoryImpl(this._datasource);
final ContainersDatasource _datasource;
@override
Future<List<Container>> getContainers() async {
final models = await _datasource.getContainers();
return models.map((m) => m.toEntity()).toList();
}
@override
Future<List<Container>> getContainersByStack(String stackId) async {
final containers = await getContainers();
return containers.where((c) => c.stackId == stackId).toList();
}
@override
Future<Container> getContainer(String id) async {
final model = await _datasource.getContainer(id);
return model.toEntity();
}
@override
Future<void> startContainer(String id) async {
await _datasource.containerAction(id, 'start');
}
@override
Future<void> stopContainer(String id) async {
await _datasource.containerAction(id, 'stop');
}
@override
Future<void> restartContainer(String id) async {
await _datasource.containerAction(id, 'restart');
}
@override
Future<void> pauseContainer(String id) async {
await _datasource.containerAction(id, 'pause');
}
@override
Future<void> unpauseContainer(String id) async {
await _datasource.containerAction(id, 'unpause');
}
@override
Future<void> removeContainer(String id, {bool force = false}) async {
await _datasource.removeContainer(id, force: force);
}
@override
Future<String> getContainerLogs(
String id, {
int? tail,
bool timestamps = false,
}) async {
return _datasource.getContainerLogs(id, tail: tail, timestamps: timestamps);
}
@override
Stream<String> streamContainerLogs(String id, {bool timestamps = false}) {
// TODO: Implement WebSocket/SSE streaming
throw UnimplementedError('Log streaming not yet implemented');
}
}
/// Provides the container repository.
@riverpod
ContainerRepository containerRepository(ContainerRepositoryRef ref) {
final datasource = ref.watch(containersDatasourceProvider);
return ContainerRepositoryImpl(datasource);
}