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>
18 KiB
18 KiB
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
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
@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)
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
@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
// 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
- Memory Cache: In-memory Map for frequently accessed data
- Disk Cache (Hive): Persistent storage for offline access
- Secure Storage: Encrypted storage for tokens and credentials
Cache Policy
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
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
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
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
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
# build.yaml
targets:
$default:
builders:
freezed:
enabled: true
json_serializable:
enabled: true
Environment Configuration
// 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
# .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_imagewith disk and memory cache - Progressive JPEG for large images
- Thumbnail generation for lists
List Performance
- Use
ListView.builderfor 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