docs: add Phase 0 documentation
- docs/ARCHITECTURE.md - Clean Architecture patterns and conventions - docs/API_INTEGRATION.md - Core API and Tatlock API endpoints - docs/DEPLOYMENT.md - Docker, NPM, Portainer setup - docs/DATAGRID.md - DataGrid component specification - docs/THEMING.md - Material 3 theming guide Also update seed color to teal for consistency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
# Architecture Guide
|
||||
|
||||
This document defines the Clean Architecture patterns and conventions used in Tatlock UI.
|
||||
|
||||
## Overview
|
||||
|
||||
Tatlock UI follows **Clean Architecture** with feature-based organization. This separation ensures testability, maintainability, and clear boundaries between concerns.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart # Entry point
|
||||
├── version.g.dart # Generated version info
|
||||
├── app.dart # MaterialApp.router setup
|
||||
├── core/ # Shared infrastructure
|
||||
│ ├── api/ # HTTP client, interceptors, SSE
|
||||
│ ├── auth/ # Authentik OIDC integration
|
||||
│ ├── config/ # Environment configuration
|
||||
│ ├── error/ # Error types and handling
|
||||
│ └── theme/ # Material 3 theming
|
||||
├── routing/ # go_router configuration
|
||||
├── shared/ # Reusable components
|
||||
│ ├── components/ # DataGrid, common widgets
|
||||
│ └── layouts/ # App scaffold, navigation
|
||||
└── features/ # Feature modules
|
||||
├── dashboard/
|
||||
├── chat/
|
||||
├── containers/
|
||||
└── housekeeping/
|
||||
```
|
||||
|
||||
## Feature Structure
|
||||
|
||||
Each feature follows a three-layer architecture:
|
||||
|
||||
```
|
||||
features/{feature}/
|
||||
├── presentation/ # UI Layer
|
||||
│ ├── pages/ # Full-screen route widgets
|
||||
│ ├── widgets/ # Feature-specific widgets
|
||||
│ └── providers/ # Riverpod providers/notifiers
|
||||
├── domain/ # Business Logic Layer
|
||||
│ ├── entities/ # Immutable business objects
|
||||
│ ├── repositories/ # Abstract repository interfaces
|
||||
│ └── usecases/ # Single-purpose business operations
|
||||
└── data/ # Data Layer
|
||||
├── models/ # JSON-serializable DTOs
|
||||
├── datasources/ # API clients, local storage
|
||||
└── repositories/ # Repository implementations
|
||||
```
|
||||
|
||||
## Layer Rules
|
||||
|
||||
### Domain Layer (Pure Dart)
|
||||
|
||||
The domain layer is the core of each feature. It has **no dependencies** on Flutter, external packages, or other layers.
|
||||
|
||||
**Entities** - Immutable business objects:
|
||||
```dart
|
||||
// domain/entities/container.dart
|
||||
class Container {
|
||||
const Container({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.status,
|
||||
required this.image,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final ContainerStatus status;
|
||||
final String image;
|
||||
}
|
||||
|
||||
enum ContainerStatus { running, stopped, paused, restarting }
|
||||
```
|
||||
|
||||
**Repository Interfaces** - Abstract contracts:
|
||||
```dart
|
||||
// domain/repositories/container_repository.dart
|
||||
abstract class ContainerRepository {
|
||||
Future<List<Container>> getContainers();
|
||||
Future<Container> getContainer(String id);
|
||||
Future<void> startContainer(String id);
|
||||
Future<void> stopContainer(String id);
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases** - Single-purpose operations (optional, for complex logic):
|
||||
```dart
|
||||
// domain/usecases/restart_unhealthy_containers.dart
|
||||
class RestartUnhealthyContainers {
|
||||
RestartUnhealthyContainers(this._repository);
|
||||
|
||||
final ContainerRepository _repository;
|
||||
|
||||
Future<int> call() async {
|
||||
final containers = await _repository.getContainers();
|
||||
final unhealthy = containers.where((c) => c.status == ContainerStatus.stopped);
|
||||
|
||||
for (final container in unhealthy) {
|
||||
await _repository.startContainer(container.id);
|
||||
}
|
||||
|
||||
return unhealthy.length;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Data Layer (External Dependencies)
|
||||
|
||||
The data layer implements domain interfaces and handles external communication.
|
||||
|
||||
**Models** - JSON-serializable DTOs:
|
||||
```dart
|
||||
// data/models/container_model.dart
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'container_model.freezed.dart';
|
||||
part 'container_model.g.dart';
|
||||
|
||||
@freezed
|
||||
class ContainerModel with _$ContainerModel {
|
||||
const factory ContainerModel({
|
||||
required String id,
|
||||
required String name,
|
||||
required String status,
|
||||
required String image,
|
||||
}) = _ContainerModel;
|
||||
|
||||
factory ContainerModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ContainerModelFromJson(json);
|
||||
}
|
||||
|
||||
extension ContainerModelX on ContainerModel {
|
||||
Container toEntity() => Container(
|
||||
id: id,
|
||||
name: name,
|
||||
status: ContainerStatus.values.byName(status),
|
||||
image: image,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Data Sources** - API clients:
|
||||
```dart
|
||||
// data/datasources/containers_datasource.dart
|
||||
class ContainersDatasource {
|
||||
ContainersDatasource(this._client);
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
Future<List<ContainerModel>> getContainers() async {
|
||||
final response = await _client.get('/infrastructure/containers');
|
||||
return (response.data as List)
|
||||
.map((json) => ContainerModel.fromJson(json))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Repository Implementations**:
|
||||
```dart
|
||||
// data/repositories/container_repository_impl.dart
|
||||
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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Presentation Layer (Flutter + Riverpod)
|
||||
|
||||
The presentation layer contains UI code and state management.
|
||||
|
||||
**Providers** - Riverpod state management:
|
||||
```dart
|
||||
// presentation/providers/containers_provider.dart
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'containers_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ContainersNotifier extends _$ContainersNotifier {
|
||||
@override
|
||||
Future<List<Container>> build() async {
|
||||
final repository = ref.watch(containerRepositoryProvider);
|
||||
return repository.getContainers();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() => ref.read(containerRepositoryProvider).getContainers());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pages** - Full-screen routes:
|
||||
```dart
|
||||
// presentation/pages/containers_page.dart
|
||||
class ContainersPage extends ConsumerWidget {
|
||||
const ContainersPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final containersAsync = ref.watch(containersNotifierProvider);
|
||||
|
||||
return containersAsync.when(
|
||||
data: (containers) => ContainersList(containers: containers),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => ErrorDisplay(error: error),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Widgets** - Feature-specific UI components:
|
||||
```dart
|
||||
// presentation/widgets/container_card.dart
|
||||
class ContainerCard extends StatelessWidget {
|
||||
const ContainerCard({super.key, required this.container});
|
||||
|
||||
final Container container;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Widget implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Flow
|
||||
|
||||
```
|
||||
Presentation → Domain ← Data
|
||||
↓ ↑ ↓
|
||||
Riverpod Interfaces Dio/API
|
||||
```
|
||||
|
||||
- Presentation depends on Domain (uses entities, calls repository interfaces)
|
||||
- Data depends on Domain (implements repository interfaces)
|
||||
- Domain depends on nothing (pure Dart)
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Use Riverpod for all dependency injection:
|
||||
|
||||
```dart
|
||||
// Datasource provider
|
||||
@riverpod
|
||||
ContainersDatasource containersDatasource(Ref ref) {
|
||||
return ContainersDatasource(ref.watch(apiClientProvider));
|
||||
}
|
||||
|
||||
// Repository provider (returns interface type)
|
||||
@riverpod
|
||||
ContainerRepository containerRepository(Ref ref) {
|
||||
return ContainerRepositoryImpl(ref.watch(containersDatasourceProvider));
|
||||
}
|
||||
```
|
||||
|
||||
## File Naming Conventions
|
||||
|
||||
| Type | Convention | Example |
|
||||
|------|------------|---------|
|
||||
| Pages | `{name}_page.dart` | `containers_page.dart` |
|
||||
| Widgets | `{name}_{type}.dart` | `container_card.dart` |
|
||||
| Providers | `{name}_provider.dart` | `containers_provider.dart` |
|
||||
| Entities | `{name}.dart` | `container.dart` |
|
||||
| Models | `{name}_model.dart` | `container_model.dart` |
|
||||
| Repositories | `{name}_repository.dart` | `container_repository.dart` |
|
||||
| Datasources | `{name}_datasource.dart` | `containers_datasource.dart` |
|
||||
| Use Cases | `{action}_{noun}.dart` | `restart_container.dart` |
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
Each layer has specific testing requirements:
|
||||
|
||||
### Domain Layer
|
||||
- **Unit tests** for all entities and use cases
|
||||
- No mocking needed (pure Dart)
|
||||
- 100% coverage expected
|
||||
|
||||
### Data Layer
|
||||
- **Unit tests** for model serialization
|
||||
- **Integration tests** for datasources (mock HTTP)
|
||||
- Test entity conversions
|
||||
|
||||
### Presentation Layer
|
||||
- **Widget tests** for all pages and complex widgets
|
||||
- **Provider tests** for state management logic
|
||||
- Mock repositories using `mocktail`
|
||||
|
||||
## Code Generation
|
||||
|
||||
Run after modifying annotated code:
|
||||
|
||||
```bash
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
Generated files:
|
||||
- `*.freezed.dart` - Immutable classes
|
||||
- `*.g.dart` - JSON serialization, Riverpod providers
|
||||
|
||||
## Import Rules
|
||||
|
||||
1. Never import from `data/` in `domain/`
|
||||
2. Never import from `presentation/` in `domain/` or `data/`
|
||||
3. Import entities from `domain/`, not models from `data/`
|
||||
4. Use relative imports within a feature, package imports across features
|
||||
|
||||
```dart
|
||||
// Good - within feature
|
||||
import '../domain/entities/container.dart';
|
||||
|
||||
// Good - across features
|
||||
import 'package:tatlock_ui/features/containers/domain/entities/container.dart';
|
||||
|
||||
// Bad - importing model in presentation
|
||||
import '../data/models/container_model.dart'; // Don't do this
|
||||
```
|
||||
Reference in New Issue
Block a user