This MVP release provides a complete full-stack solution for managing action mappings in Geutebruck's GeViScope and GeViSoft video surveillance systems. ## Features ### Flutter Web Application (Port 8081) - Modern, responsive UI for managing action mappings - Action picker dialog with full parameter configuration - Support for both GSC (GeViScope) and G-Core server actions - Consistent UI for input and output actions with edit/delete capabilities - Real-time action mapping creation, editing, and deletion - Server categorization (GSC: prefix for GeViScope, G-Core: prefix for G-Core servers) ### FastAPI REST Backend (Port 8000) - RESTful API for action mapping CRUD operations - Action template service with comprehensive action catalog (247 actions) - Server management (G-Core and GeViScope servers) - Configuration tree reading and writing - JWT authentication with role-based access control - PostgreSQL database integration ### C# SDK Bridge (gRPC, Port 50051) - Native integration with GeViSoft SDK (GeViProcAPINET_4_0.dll) - Action mapping creation with correct binary format - Support for GSC and G-Core action types - Proper Camera parameter inclusion in action strings (fixes CrossSwitch bug) - Action ID lookup table with server-specific action IDs - Configuration reading/writing via SetupClient ## Bug Fixes - **CrossSwitch Bug**: GSC and G-Core actions now correctly display camera/PTZ head parameters in GeViSet - Action strings now include Camera parameter: `@ PanLeft (Comment: "", Camera: 101028)` - Proper filter flags and VideoInput=0 for action mappings - Correct action ID assignment (4198 for GSC, 9294 for G-Core PanLeft) ## Technical Stack - **Frontend**: Flutter Web, Dart, Dio HTTP client - **Backend**: Python FastAPI, PostgreSQL, Redis - **SDK Bridge**: C# .NET 8.0, gRPC, GeViSoft SDK - **Authentication**: JWT tokens - **Configuration**: GeViSoft .set files (binary format) ## Credentials - GeViSoft/GeViScope: username=sysadmin, password=masterkey - Default admin: username=admin, password=admin123 ## Deployment All services run on localhost: - Flutter Web: http://localhost:8081 - FastAPI: http://localhost:8000 - SDK Bridge gRPC: localhost:50051 - GeViServer: localhost (default port) Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
619 lines
18 KiB
Markdown
619 lines
18 KiB
Markdown
# Geutebruck API Flutter App - Technical Implementation Plan
|
|
|
|
## Technology Stack
|
|
|
|
### Core Framework
|
|
- **Flutter 3.24+**: Cross-platform mobile framework
|
|
- **Dart 3.0+**: Programming language with sound null safety
|
|
|
|
### State Management
|
|
- **flutter_bloc 8.1+**: BLoC pattern implementation
|
|
- *Rationale*: Predictable state management, excellent testing support, separation of concerns
|
|
- *Alternatives considered*: Provider (simpler but less structured), Riverpod (newer but less mature ecosystem)
|
|
|
|
### HTTP Client & API
|
|
- **dio 5.4+**: HTTP client for API calls
|
|
- Features: Interceptors, request/response transformation, timeout handling
|
|
- Better than http package: More features, better error handling
|
|
- **retrofit 4.0+**: Type-safe HTTP client generator
|
|
- Auto-generates API client code from interface definitions
|
|
- Reduces boilerplate and errors
|
|
|
|
### Data Persistence
|
|
- **flutter_secure_storage 9.0+**: Secure storage for tokens and credentials
|
|
- **shared_preferences 2.2+**: App settings and preferences
|
|
- **hive 2.2+**: Local database for caching
|
|
- *Rationale*: Fast, lightweight, no SQL required, perfect for caching API responses
|
|
|
|
### Dependency Injection
|
|
- **get_it 7.6+**: Service locator pattern
|
|
- *Rationale*: Simple, explicit dependencies, excellent for testing
|
|
- **injectable 2.3+**: Code generation for get_it
|
|
- Reduces boilerplate in dependency registration
|
|
|
|
### UI Components
|
|
- **Material Design 3**: Modern, accessible UI components
|
|
- **cached_network_image 3.3+**: Efficient image loading and caching
|
|
- **shimmer 3.0+**: Loading skeletons
|
|
- **flutter_svg 2.0+**: SVG image support
|
|
|
|
### Navigation
|
|
- **go_router 13.0+**: Declarative routing
|
|
- *Rationale*: Deep linking support, type-safe navigation, excellent for web
|
|
|
|
### Form Handling & Validation
|
|
- **flutter_form_builder 9.1+**: Dynamic form creation
|
|
- **form_builder_validators 9.1+**: Reusable validators
|
|
|
|
### Testing
|
|
- **mockito 5.4+**: Mocking dependencies
|
|
- **bloc_test 9.1+**: Testing BLoCs
|
|
- **golden_toolkit 0.15+**: Widget screenshot testing
|
|
|
|
### Code Generation
|
|
- **freezed 2.4+**: Immutable data classes with union types
|
|
- **json_serializable 6.7+**: JSON serialization
|
|
- **build_runner 2.4+**: Code generation orchestrator
|
|
|
|
### Development Tools
|
|
- **flutter_launcher_icons 0.13+**: App icon generation
|
|
- **flutter_native_splash 2.3+**: Splash screen generation
|
|
- **very_good_analysis 5.1+**: Lint rules
|
|
|
|
## Architecture
|
|
|
|
### Clean Architecture + BLoC
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Presentation Layer │
|
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
|
│ │ Screens │ │ Widgets │ │ BLoCs │ │
|
|
│ └────────────┘ └────────────┘ └────────────┘ │
|
|
└─────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Domain Layer │
|
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
|
│ │ Entities │ │ Use Cases │ │ Repository │ │
|
|
│ │ │ │ │ │ Interfaces │ │
|
|
│ └────────────┘ └────────────┘ └────────────┘ │
|
|
└─────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Data Layer │
|
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
|
│ │ Models │ │Repositories│ │Data Sources│ │
|
|
│ │ │ │ │ │ (Remote) │ │
|
|
│ └────────────┘ └────────────┘ └────────────┘ │
|
|
└─────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Layer Responsibilities
|
|
|
|
#### Presentation Layer
|
|
- **Screens**: Full-page views (ServerListScreen, LoginScreen, etc.)
|
|
- **Widgets**: Reusable UI components (ServerCard, LoadingWidget, etc.)
|
|
- **BLoCs**: Business logic controllers, emit states based on events
|
|
|
|
#### Domain Layer
|
|
- **Entities**: Pure business objects (Server, ActionMapping, Camera)
|
|
- **Use Cases**: Single-responsibility business operations (GetServers, CreateServer)
|
|
- **Repository Interfaces**: Contracts for data access
|
|
|
|
#### Data Layer
|
|
- **Models**: Data transfer objects with JSON serialization
|
|
- **Repositories**: Implement repository interfaces, coordinate data sources
|
|
- **Data Sources**:
|
|
- **Remote**: API client using dio/retrofit
|
|
- **Local**: Hive cache
|
|
|
|
### Folder Structure
|
|
|
|
```
|
|
lib/
|
|
├── core/
|
|
│ ├── constants/
|
|
│ │ ├── api_constants.dart
|
|
│ │ ├── app_constants.dart
|
|
│ │ └── asset_constants.dart
|
|
│ ├── errors/
|
|
│ │ ├── exceptions.dart
|
|
│ │ └── failures.dart
|
|
│ ├── network/
|
|
│ │ ├── api_client.dart
|
|
│ │ ├── dio_client.dart
|
|
│ │ └── interceptors/
|
|
│ │ ├── auth_interceptor.dart
|
|
│ │ └── logging_interceptor.dart
|
|
│ ├── theme/
|
|
│ │ ├── app_theme.dart
|
|
│ │ └── colors.dart
|
|
│ └── utils/
|
|
│ ├── validators.dart
|
|
│ └── extensions.dart
|
|
├── data/
|
|
│ ├── models/
|
|
│ │ ├── server_model.dart
|
|
│ │ ├── action_mapping_model.dart
|
|
│ │ ├── camera_model.dart
|
|
│ │ └── auth_model.dart
|
|
│ ├── repositories/
|
|
│ │ ├── server_repository_impl.dart
|
|
│ │ ├── action_mapping_repository_impl.dart
|
|
│ │ ├── camera_repository_impl.dart
|
|
│ │ └── auth_repository_impl.dart
|
|
│ └── data_sources/
|
|
│ ├── remote/
|
|
│ │ ├── server_remote_data_source.dart
|
|
│ │ ├── action_mapping_remote_data_source.dart
|
|
│ │ └── auth_remote_data_source.dart
|
|
│ └── local/
|
|
│ ├── cache_manager.dart
|
|
│ └── secure_storage_manager.dart
|
|
├── domain/
|
|
│ ├── entities/
|
|
│ │ ├── server.dart
|
|
│ │ ├── action_mapping.dart
|
|
│ │ ├── camera.dart
|
|
│ │ └── user.dart
|
|
│ ├── repositories/
|
|
│ │ ├── server_repository.dart
|
|
│ │ ├── action_mapping_repository.dart
|
|
│ │ └── auth_repository.dart
|
|
│ └── use_cases/
|
|
│ ├── servers/
|
|
│ │ ├── get_servers.dart
|
|
│ │ ├── create_server.dart
|
|
│ │ ├── update_server.dart
|
|
│ │ └── delete_server.dart
|
|
│ ├── action_mappings/
|
|
│ │ ├── get_action_mappings.dart
|
|
│ │ └── create_action_mapping.dart
|
|
│ └── auth/
|
|
│ ├── login.dart
|
|
│ ├── refresh_token.dart
|
|
│ └── logout.dart
|
|
├── presentation/
|
|
│ ├── blocs/
|
|
│ │ ├── auth/
|
|
│ │ │ ├── auth_bloc.dart
|
|
│ │ │ ├── auth_event.dart
|
|
│ │ │ └── auth_state.dart
|
|
│ │ ├── server/
|
|
│ │ │ ├── server_bloc.dart
|
|
│ │ │ ├── server_event.dart
|
|
│ │ │ └── server_state.dart
|
|
│ │ └── action_mapping/
|
|
│ │ ├── action_mapping_bloc.dart
|
|
│ │ ├── action_mapping_event.dart
|
|
│ │ └── action_mapping_state.dart
|
|
│ ├── screens/
|
|
│ │ ├── auth/
|
|
│ │ │ └── login_screen.dart
|
|
│ │ ├── servers/
|
|
│ │ │ ├── server_list_screen.dart
|
|
│ │ │ ├── server_detail_screen.dart
|
|
│ │ │ └── server_form_screen.dart
|
|
│ │ ├── action_mappings/
|
|
│ │ │ ├── action_mapping_list_screen.dart
|
|
│ │ │ └── action_mapping_form_screen.dart
|
|
│ │ ├── cameras/
|
|
│ │ │ ├── camera_list_screen.dart
|
|
│ │ │ └── camera_control_screen.dart
|
|
│ │ └── settings/
|
|
│ │ └── settings_screen.dart
|
|
│ └── widgets/
|
|
│ ├── common/
|
|
│ │ ├── loading_widget.dart
|
|
│ │ ├── error_widget.dart
|
|
│ │ └── empty_state_widget.dart
|
|
│ ├── server/
|
|
│ │ └── server_card.dart
|
|
│ └── action_mapping/
|
|
│ └── action_mapping_card.dart
|
|
├── injection.dart
|
|
└── main.dart
|
|
```
|
|
|
|
## API Integration
|
|
|
|
### Base Configuration
|
|
|
|
```dart
|
|
class ApiConfig {
|
|
static const String baseUrl = 'http://localhost:8000';
|
|
static const Duration timeout = Duration(seconds: 30);
|
|
static const Duration receiveTimeout = Duration(seconds: 30);
|
|
}
|
|
```
|
|
|
|
### Dio Setup with Interceptors
|
|
|
|
```dart
|
|
@singleton
|
|
class DioClient {
|
|
final Dio _dio;
|
|
|
|
DioClient() : _dio = Dio(BaseOptions(
|
|
baseUrl: ApiConfig.baseUrl,
|
|
connectTimeout: ApiConfig.timeout,
|
|
receiveTimeout: ApiConfig.receiveTimeout,
|
|
)) {
|
|
_dio.interceptors.addAll([
|
|
AuthInterceptor(),
|
|
LoggingInterceptor(),
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Auth Interceptor (Token Management)
|
|
|
|
```dart
|
|
class AuthInterceptor extends Interceptor {
|
|
@override
|
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
|
final token = await secureStorage.read(key: 'access_token');
|
|
if (token != null) {
|
|
options.headers['Authorization'] = 'Bearer $token';
|
|
}
|
|
handler.next(options);
|
|
}
|
|
|
|
@override
|
|
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
|
if (err.response?.statusCode == 401) {
|
|
// Token expired, try refresh
|
|
final refreshed = await refreshToken();
|
|
if (refreshed) {
|
|
// Retry original request
|
|
return handler.resolve(await _retry(err.requestOptions));
|
|
}
|
|
}
|
|
handler.next(err);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Repository Pattern Example
|
|
|
|
```dart
|
|
@injectable
|
|
class ServerRepositoryImpl implements ServerRepository {
|
|
final ServerRemoteDataSource remoteDataSource;
|
|
final CacheManager cacheManager;
|
|
|
|
ServerRepositoryImpl({
|
|
required this.remoteDataSource,
|
|
required this.cacheManager,
|
|
});
|
|
|
|
@override
|
|
Future<Either<Failure, List<Server>>> getServers() async {
|
|
try {
|
|
// Try cache first
|
|
final cached = await cacheManager.getServers();
|
|
if (cached != null && !cached.isExpired) {
|
|
return Right(cached.data);
|
|
}
|
|
|
|
// Fetch from API
|
|
final servers = await remoteDataSource.getServers();
|
|
|
|
// Update cache
|
|
await cacheManager.saveServers(servers);
|
|
|
|
return Right(servers.map((m) => m.toEntity()).toList());
|
|
} on ServerException catch (e) {
|
|
return Left(ServerFailure(e.message));
|
|
} on NetworkException {
|
|
return Left(NetworkFailure());
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## State Management Flow
|
|
|
|
### BLoC Pattern
|
|
|
|
```dart
|
|
// Event
|
|
abstract class ServerEvent {}
|
|
class LoadServers extends ServerEvent {}
|
|
class CreateServer extends ServerEvent {
|
|
final ServerCreateRequest request;
|
|
CreateServer(this.request);
|
|
}
|
|
|
|
// State
|
|
abstract class ServerState {}
|
|
class ServerInitial extends ServerState {}
|
|
class ServerLoading extends ServerState {}
|
|
class ServerLoaded extends ServerState {
|
|
final List<Server> servers;
|
|
ServerLoaded(this.servers);
|
|
}
|
|
class ServerError extends ServerState {
|
|
final String message;
|
|
ServerError(this.message);
|
|
}
|
|
|
|
// BLoC
|
|
class ServerBloc extends Bloc<ServerEvent, ServerState> {
|
|
final GetServers getServers;
|
|
final CreateServer createServer;
|
|
|
|
ServerBloc({
|
|
required this.getServers,
|
|
required this.createServer,
|
|
}) : super(ServerInitial()) {
|
|
on<LoadServers>(_onLoadServers);
|
|
on<CreateServer>(_onCreateServer);
|
|
}
|
|
|
|
Future<void> _onLoadServers(
|
|
LoadServers event,
|
|
Emitter<ServerState> emit,
|
|
) async {
|
|
emit(ServerLoading());
|
|
final result = await getServers();
|
|
result.fold(
|
|
(failure) => emit(ServerError(failure.message)),
|
|
(servers) => emit(ServerLoaded(servers)),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Caching Strategy
|
|
|
|
### Cache Layers
|
|
|
|
1. **Memory Cache**: In-memory Map for frequently accessed data
|
|
2. **Disk Cache (Hive)**: Persistent storage for offline access
|
|
3. **Secure Storage**: Encrypted storage for tokens and credentials
|
|
|
|
### Cache Policy
|
|
|
|
```dart
|
|
class CachePolicy {
|
|
// Short-lived cache (5 minutes)
|
|
static const Duration shortCache = Duration(minutes: 5);
|
|
|
|
// Medium cache (30 minutes)
|
|
static const Duration mediumCache = Duration(minutes: 30);
|
|
|
|
// Long cache (24 hours)
|
|
static const Duration longCache = Duration(hours: 24);
|
|
}
|
|
|
|
// Server list: Short cache (changes frequently)
|
|
// Action mappings: Medium cache
|
|
// Configuration tree: Long cache
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Error Types
|
|
|
|
```dart
|
|
abstract class Failure {
|
|
String get message;
|
|
}
|
|
|
|
class NetworkFailure extends Failure {
|
|
@override
|
|
String get message => 'No internet connection';
|
|
}
|
|
|
|
class ServerFailure extends Failure {
|
|
final String errorMessage;
|
|
ServerFailure(this.errorMessage);
|
|
|
|
@override
|
|
String get message => errorMessage;
|
|
}
|
|
|
|
class ValidationFailure extends Failure {
|
|
final Map<String, String> errors;
|
|
ValidationFailure(this.errors);
|
|
|
|
@override
|
|
String get message => 'Validation failed';
|
|
}
|
|
```
|
|
|
|
### UI Error Display
|
|
|
|
```dart
|
|
Widget buildError(Failure failure) {
|
|
if (failure is NetworkFailure) {
|
|
return ErrorWidget(
|
|
message: failure.message,
|
|
icon: Icons.wifi_off,
|
|
action: ElevatedButton(
|
|
onPressed: () => context.read<ServerBloc>().add(LoadServers()),
|
|
child: Text('Retry'),
|
|
),
|
|
);
|
|
}
|
|
// ... other failure types
|
|
}
|
|
```
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
|
|
```dart
|
|
void main() {
|
|
late ServerRepositoryImpl repository;
|
|
late MockServerRemoteDataSource mockRemoteDataSource;
|
|
late MockCacheManager mockCacheManager;
|
|
|
|
setUp(() {
|
|
mockRemoteDataSource = MockServerRemoteDataSource();
|
|
mockCacheManager = MockCacheManager();
|
|
repository = ServerRepositoryImpl(
|
|
remoteDataSource: mockRemoteDataSource,
|
|
cacheManager: mockCacheManager,
|
|
);
|
|
});
|
|
|
|
group('getServers', () {
|
|
test('should return cached data when cache is valid', () async {
|
|
// Arrange
|
|
when(mockCacheManager.getServers())
|
|
.thenAnswer((_) async => CachedData([server1, server2]));
|
|
|
|
// Act
|
|
final result = await repository.getServers();
|
|
|
|
// Assert
|
|
expect(result, isA<Right<Failure, List<Server>>>());
|
|
verifyNever(mockRemoteDataSource.getServers());
|
|
});
|
|
});
|
|
}
|
|
```
|
|
|
|
### Widget Tests
|
|
|
|
```dart
|
|
void main() {
|
|
testWidgets('ServerListScreen displays servers', (tester) async {
|
|
// Arrange
|
|
final mockBloc = MockServerBloc();
|
|
when(mockBloc.state).thenReturn(ServerLoaded([server1, server2]));
|
|
|
|
// Act
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: BlocProvider<ServerBloc>.value(
|
|
value: mockBloc,
|
|
child: ServerListScreen(),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Assert
|
|
expect(find.text('Server 1'), findsOneWidget);
|
|
expect(find.text('Server 2'), findsOneWidget);
|
|
});
|
|
}
|
|
```
|
|
|
|
## Build & Deployment
|
|
|
|
### Build Configurations
|
|
|
|
```yaml
|
|
# build.yaml
|
|
targets:
|
|
$default:
|
|
builders:
|
|
freezed:
|
|
enabled: true
|
|
json_serializable:
|
|
enabled: true
|
|
```
|
|
|
|
### Environment Configuration
|
|
|
|
```dart
|
|
// config.dart
|
|
abstract class Config {
|
|
static String get apiBaseUrl => _apiBaseUrl;
|
|
static String _apiBaseUrl = 'http://localhost:8000';
|
|
|
|
static void setDevelopment() {
|
|
_apiBaseUrl = 'http://localhost:8000';
|
|
}
|
|
|
|
static void setProduction() {
|
|
_apiBaseUrl = 'https://api.production.com';
|
|
}
|
|
}
|
|
```
|
|
|
|
### CI/CD Pipeline
|
|
|
|
```yaml
|
|
# .github/workflows/flutter.yml
|
|
name: Flutter CI
|
|
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
- uses: subosito/flutter-action@v2
|
|
- run: flutter pub get
|
|
- run: flutter analyze
|
|
- run: flutter test --coverage
|
|
- uses: codecov/codecov-action@v3
|
|
```
|
|
|
|
## Implementation Phases
|
|
|
|
### Phase 1: Foundation (Week 1-2)
|
|
- Project setup with dependencies
|
|
- Folder structure
|
|
- Dependency injection setup
|
|
- API client configuration
|
|
- Authentication flow
|
|
|
|
### Phase 2: Core Features (Week 3-4)
|
|
- Server management (list, create, update, delete)
|
|
- Action mapping management
|
|
- Camera list and details
|
|
|
|
### Phase 3: Advanced Features (Week 5-6)
|
|
- PTZ camera control
|
|
- Monitor management
|
|
- Cross-switching
|
|
- Configuration export
|
|
|
|
### Phase 4: Polish (Week 7-8)
|
|
- Offline support
|
|
- Error handling improvements
|
|
- Performance optimization
|
|
- UI/UX refinements
|
|
- Accessibility improvements
|
|
|
|
### Phase 5: Testing & Deployment (Week 9-10)
|
|
- Comprehensive testing
|
|
- User acceptance testing
|
|
- Bug fixes
|
|
- App store preparation
|
|
- Documentation
|
|
|
|
## Performance Optimization
|
|
|
|
### Image Loading
|
|
- Use `cached_network_image` with disk and memory cache
|
|
- Progressive JPEG for large images
|
|
- Thumbnail generation for lists
|
|
|
|
### List Performance
|
|
- Use `ListView.builder` for long lists
|
|
- Implement pagination for server/mapping lists
|
|
- Lazy loading of details
|
|
|
|
### Memory Management
|
|
- Dispose BLoCs and controllers properly
|
|
- Clear cache periodically
|
|
- Monitor memory usage in DevTools
|
|
|
|
### Network Optimization
|
|
- Batch API requests where possible
|
|
- Implement request debouncing for search
|
|
- Cancel pending requests on navigation
|
|
- Use HTTP/2 for multiplexing
|