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>
This commit is contained in:
Administrator
2025-12-31 18:10:54 +01:00
commit 14893e62a5
4189 changed files with 1395076 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
import 'package:dartz/dartz.dart';
import '../../domain/entities/action_mapping.dart';
import '../../domain/repositories/action_mapping_repository.dart';
import '../../core/errors/failures.dart';
import '../data_sources/local/action_mapping_local_data_source.dart';
import '../data_sources/remote/action_mapping_remote_data_source.dart';
import '../models/action_mapping_hive_model.dart';
import '../services/sync_service.dart';
class ActionMappingRepositoryImpl implements ActionMappingRepository {
final ActionMappingLocalDataSource localDataSource;
final ActionMappingRemoteDataSource remoteDataSource;
final SyncService syncService;
ActionMappingRepositoryImpl({
required this.localDataSource,
required this.remoteDataSource,
required this.syncService,
});
@override
Future<Either<Failure, List<ActionMapping>>> getAllActionMappings() async {
try {
// Read from local storage
final mappings = await localDataSource.getAllActionMappings();
return Right(mappings.map((model) => model.toEntity()).toList());
} catch (e) {
return Left(ActionMappingFailure('Failed to load action mappings: $e'));
}
}
@override
Future<Either<Failure, ActionMapping>> getActionMappingById(String id) async {
try {
final mapping = await localDataSource.getActionMappingById(id);
if (mapping == null) {
return Left(ActionMappingFailure('Action mapping not found'));
}
return Right(mapping.toEntity());
} catch (e) {
return Left(ActionMappingFailure('Failed to load action mapping: $e'));
}
}
@override
Future<Either<Failure, void>> createActionMapping(ActionMapping mapping) async {
try {
// Save to local storage with dirty flag and 'create' operation
final hiveModel = ActionMappingHiveModel.fromEntity(
mapping,
isDirty: true,
syncOperation: 'create',
);
await localDataSource.saveActionMapping(hiveModel);
return const Right(null);
} catch (e) {
return Left(ActionMappingFailure('Failed to create action mapping: $e'));
}
}
@override
Future<Either<Failure, void>> updateActionMapping(ActionMapping mapping) async {
try {
// Save to local storage with dirty flag and 'update' operation
final hiveModel = ActionMappingHiveModel.fromEntity(
mapping,
isDirty: true,
syncOperation: 'update',
);
await localDataSource.saveActionMapping(hiveModel);
return const Right(null);
} catch (e) {
return Left(ActionMappingFailure('Failed to update action mapping: $e'));
}
}
@override
Future<Either<Failure, void>> deleteActionMapping(String id) async {
try {
// Mark as deleted in local storage (soft delete with sync flag)
await localDataSource.deleteActionMapping(id);
return const Right(null);
} catch (e) {
return Left(ActionMappingFailure('Failed to delete action mapping: $e'));
}
}
@override
Future<Either<Failure, List<ActionMapping>>> searchActionMappings(String query) async {
try {
final mappings = await localDataSource.searchActionMappings(query);
return Right(mappings.map((model) => model.toEntity()).toList());
} catch (e) {
return Left(ActionMappingFailure('Failed to search action mappings: $e'));
}
}
@override
Future<Either<Failure, SyncResult>> syncToServer() async {
return await syncService.syncActionMappings();
}
@override
Future<Either<Failure, int>> downloadFromServer() async {
return await syncService.downloadActionMappings();
}
@override
Future<Either<Failure, int>> getDirtyCount() async {
try {
final dirtyMappings = await localDataSource.getDirtyActionMappings();
return Right(dirtyMappings.length);
} catch (e) {
return Left(ActionMappingFailure('Failed to get dirty count: $e'));
}
}
}

View File

@@ -0,0 +1,94 @@
import 'package:dartz/dartz.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../../core/errors/failures.dart';
import '../../core/errors/exceptions.dart';
import '../data_sources/remote/auth_remote_data_source.dart';
import '../data_sources/local/secure_storage_manager.dart';
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource;
final SecureStorageManager storageManager;
AuthRepositoryImpl({
required this.remoteDataSource,
required this.storageManager,
});
@override
Future<Either<Failure, User>> login(String username, String password) async {
try {
final authResponse = await remoteDataSource.login(username, password);
// Save tokens and user data
await storageManager.saveAccessToken(authResponse.accessToken);
await storageManager.saveRefreshToken(authResponse.refreshToken);
await storageManager.saveUsername(authResponse.username);
await storageManager.saveUserRole(authResponse.role);
return Right(authResponse.toEntity());
} on AuthenticationException catch (e) {
return Left(AuthenticationFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error: $e'));
}
}
@override
Future<Either<Failure, void>> logout() async {
try {
await remoteDataSource.logout();
await storageManager.clearAll();
return const Right(null);
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error during logout: $e'));
}
}
@override
Future<Either<Failure, User>> refreshToken() async {
try {
final refreshToken = await storageManager.getRefreshToken();
if (refreshToken == null) {
return const Left(AuthenticationFailure('No refresh token available'));
}
// In a real implementation, you'd call an API endpoint to refresh the token
// For now, we'll just return an error
return const Left(AuthenticationFailure('Token refresh not implemented'));
} catch (e) {
return Left(ServerFailure('Failed to refresh token: $e'));
}
}
@override
Future<Either<Failure, User?>> getCurrentUser() async {
try {
final username = await storageManager.getUsername();
final role = await storageManager.getUserRole();
final accessToken = await storageManager.getAccessToken();
final refreshToken = await storageManager.getRefreshToken();
if (username == null || role == null || accessToken == null || refreshToken == null) {
return const Right(null);
}
return Right(User(
username: username,
role: role,
accessToken: accessToken,
refreshToken: refreshToken,
));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(ServerFailure('Failed to get current user: $e'));
}
}
}

View File

@@ -0,0 +1,133 @@
import 'package:dartz/dartz.dart';
import '../../domain/entities/server.dart';
import '../../domain/repositories/server_repository.dart';
import '../../core/errors/failures.dart';
import '../data_sources/local/server_local_data_source.dart';
import '../data_sources/remote/server_remote_data_source.dart';
import '../models/server_hive_model.dart';
import '../services/sync_service.dart';
class ServerRepositoryImpl implements ServerRepository {
final ServerLocalDataSource localDataSource;
final ServerRemoteDataSource remoteDataSource;
final SyncService syncService;
ServerRepositoryImpl({
required this.localDataSource,
required this.remoteDataSource,
required this.syncService,
});
@override
Future<Either<Failure, List<Server>>> getAllServers() async {
try {
// Read from local storage
final servers = await localDataSource.getAllServers();
return Right(servers.map((model) => model.toEntity()).toList());
} catch (e) {
return Left(ServerFailure('Failed to load servers: $e'));
}
}
@override
Future<Either<Failure, List<Server>>> getGCoreServers() async {
try {
// Read from local storage
final servers = await localDataSource.getServersByType('gcore');
return Right(servers.map((model) => model.toEntity()).toList());
} catch (e) {
return Left(ServerFailure('Failed to load G-Core servers: $e'));
}
}
@override
Future<Either<Failure, List<Server>>> getGeViScopeServers() async {
try {
// Read from local storage
final servers = await localDataSource.getServersByType('geviscope');
return Right(servers.map((model) => model.toEntity()).toList());
} catch (e) {
return Left(ServerFailure('Failed to load GeViScope servers: $e'));
}
}
@override
Future<Either<Failure, Server>> getServerById(String id, ServerType type) async {
try {
final typeStr = type == ServerType.gcore ? 'gcore' : 'geviscope';
final server = await localDataSource.getServerById(id, typeStr);
if (server == null) {
return Left(ServerFailure('Server not found'));
}
return Right(server.toEntity());
} catch (e) {
return Left(ServerFailure('Failed to load server: $e'));
}
}
@override
Future<Either<Failure, void>> createServer(Server server) async {
try {
// Save to local storage with dirty flag and 'create' operation
final hiveModel = ServerHiveModel.fromEntity(
server,
isDirty: true,
syncOperation: 'create',
);
await localDataSource.saveServer(hiveModel);
return const Right(null);
} catch (e) {
return Left(ServerFailure('Failed to create server: $e'));
}
}
@override
Future<Either<Failure, void>> updateServer(Server server) async {
try {
// Save to local storage with dirty flag and 'update' operation
final hiveModel = ServerHiveModel.fromEntity(
server,
isDirty: true,
syncOperation: 'update',
);
await localDataSource.saveServer(hiveModel);
return const Right(null);
} catch (e) {
return Left(ServerFailure('Failed to update server: $e'));
}
}
@override
Future<Either<Failure, void>> deleteServer(String id, ServerType type) async {
try {
final typeStr = type == ServerType.gcore ? 'gcore' : 'geviscope';
// Mark as deleted in local storage (soft delete with sync flag)
await localDataSource.deleteServer(id, typeStr);
return const Right(null);
} catch (e) {
return Left(ServerFailure('Failed to delete server: $e'));
}
}
@override
Future<Either<Failure, SyncResult>> syncToServer() async {
return await syncService.syncServers();
}
@override
Future<Either<Failure, int>> downloadFromServer() async {
return await syncService.downloadServers();
}
@override
Future<Either<Failure, int>> getDirtyCount() async {
try {
final dirtyServers = await localDataSource.getDirtyServers();
return Right(dirtyServers.length);
} catch (e) {
return Left(ServerFailure('Failed to get dirty count: $e'));
}
}
}