Files
Administrator 14893e62a5 feat: Geutebruck GeViScope/GeViSoft Action Mapping System - MVP
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>
2025-12-31 18:10:54 +01:00

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

  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

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_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