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,193 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'action_mapping_event.dart';
import 'action_mapping_state.dart';
import '../../../domain/repositories/action_mapping_repository.dart';
import '../../../data/services/sync_service.dart';
class ActionMappingBloc extends Bloc<ActionMappingEvent, ActionMappingState> {
final ActionMappingRepository actionMappingRepository;
ActionMappingBloc({required this.actionMappingRepository})
: super(const ActionMappingInitial()) {
on<LoadActionMappings>(_onLoadActionMappings);
on<SearchActionMappings>(_onSearchActionMappings);
on<CreateActionMappingEvent>(_onCreateActionMapping);
on<UpdateActionMappingEvent>(_onUpdateActionMapping);
on<DeleteActionMappingEvent>(_onDeleteActionMapping);
on<SyncActionMappingsEvent>(_onSyncActionMappings);
on<DownloadActionMappingsEvent>(_onDownloadActionMappings);
on<CheckDirtyCountEvent>(_onCheckDirtyCount);
}
Future<void> _onLoadActionMappings(
LoadActionMappings event,
Emitter<ActionMappingState> emit,
) async {
emit(const ActionMappingLoading());
final result = await actionMappingRepository.getAllActionMappings();
final dirtyCountResult = await actionMappingRepository.getDirtyCount();
result.fold(
(failure) => emit(ActionMappingError(failure.message)),
(mappings) {
final dirtyCount = dirtyCountResult.fold((_) => 0, (count) => count);
emit(ActionMappingLoaded(mappings, dirtyCount: dirtyCount));
},
);
}
Future<void> _onSearchActionMappings(
SearchActionMappings event,
Emitter<ActionMappingState> emit,
) async {
emit(const ActionMappingLoading());
final result = await actionMappingRepository.searchActionMappings(event.query);
final dirtyCountResult = await actionMappingRepository.getDirtyCount();
result.fold(
(failure) => emit(ActionMappingError(failure.message)),
(mappings) {
final dirtyCount = dirtyCountResult.fold((_) => 0, (count) => count);
emit(ActionMappingLoaded(mappings, dirtyCount: dirtyCount));
},
);
}
Future<void> _onCreateActionMapping(
CreateActionMappingEvent event,
Emitter<ActionMappingState> emit,
) async {
emit(const ActionMappingLoading());
final result = await actionMappingRepository.createActionMapping(event.mapping);
await result.fold(
(failure) async => emit(ActionMappingError(failure.message)),
(_) async {
// Reload action mappings first
final mappingsResult = await actionMappingRepository.getAllActionMappings();
final dirtyCountResult = await actionMappingRepository.getDirtyCount();
mappingsResult.fold(
(failure) => emit(ActionMappingError(failure.message)),
(mappings) {
final dirtyCount = dirtyCountResult.fold((_) => 0, (count) => count);
emit(ActionMappingLoaded(mappings, dirtyCount: dirtyCount));
},
);
},
);
}
Future<void> _onUpdateActionMapping(
UpdateActionMappingEvent event,
Emitter<ActionMappingState> emit,
) async {
emit(const ActionMappingLoading());
final result = await actionMappingRepository.updateActionMapping(event.mapping);
await result.fold(
(failure) async => emit(ActionMappingError(failure.message)),
(_) async {
// Reload action mappings first
final mappingsResult = await actionMappingRepository.getAllActionMappings();
final dirtyCountResult = await actionMappingRepository.getDirtyCount();
mappingsResult.fold(
(failure) => emit(ActionMappingError(failure.message)),
(mappings) {
final dirtyCount = dirtyCountResult.fold((_) => 0, (count) => count);
emit(ActionMappingLoaded(mappings, dirtyCount: dirtyCount));
},
);
},
);
}
Future<void> _onDeleteActionMapping(
DeleteActionMappingEvent event,
Emitter<ActionMappingState> emit,
) async {
emit(const ActionMappingLoading());
final result = await actionMappingRepository.deleteActionMapping(event.id);
await result.fold(
(failure) async => emit(ActionMappingError(failure.message)),
(_) async {
// Reload action mappings first
final mappingsResult = await actionMappingRepository.getAllActionMappings();
final dirtyCountResult = await actionMappingRepository.getDirtyCount();
mappingsResult.fold(
(failure) => emit(ActionMappingError(failure.message)),
(mappings) {
final dirtyCount = dirtyCountResult.fold((_) => 0, (count) => count);
emit(ActionMappingLoaded(mappings, dirtyCount: dirtyCount));
},
);
},
);
}
Future<void> _onSyncActionMappings(
SyncActionMappingsEvent event,
Emitter<ActionMappingState> emit,
) async {
emit(const ActionMappingSyncing());
final result = await actionMappingRepository.syncToServer();
result.fold(
(failure) => emit(ActionMappingError(failure.message)),
(syncResult) {
if (syncResult.status == SyncStatus.success) {
emit(ActionMappingSyncSuccess(
syncResult.message ?? 'Sync completed',
syncResult.syncedCount ?? 0,
));
// Reload action mappings after sync
add(const LoadActionMappings());
} else if (syncResult.status == SyncStatus.error) {
emit(ActionMappingError(syncResult.message ?? 'Sync failed'));
}
},
);
}
Future<void> _onDownloadActionMappings(
DownloadActionMappingsEvent event,
Emitter<ActionMappingState> emit,
) async {
emit(const ActionMappingDownloading());
final result = await actionMappingRepository.downloadFromServer();
result.fold(
(failure) => emit(ActionMappingError(failure.message)),
(count) {
emit(ActionMappingOperationSuccess('Downloaded $count action mappings from server'));
// Reload action mappings after download
add(const LoadActionMappings());
},
);
}
Future<void> _onCheckDirtyCount(
CheckDirtyCountEvent event,
Emitter<ActionMappingState> emit,
) async {
final result = await actionMappingRepository.getDirtyCount();
result.fold(
(failure) => emit(ActionMappingError(failure.message)),
(count) {
// Just trigger a reload which will include the dirty count
add(const LoadActionMappings());
},
);
}
}

View File

@@ -0,0 +1,61 @@
import 'package:equatable/equatable.dart';
import '../../../domain/entities/action_mapping.dart';
abstract class ActionMappingEvent extends Equatable {
const ActionMappingEvent();
@override
List<Object?> get props => [];
}
class LoadActionMappings extends ActionMappingEvent {
const LoadActionMappings();
}
class SearchActionMappings extends ActionMappingEvent {
final String query;
const SearchActionMappings(this.query);
@override
List<Object?> get props => [query];
}
class CreateActionMappingEvent extends ActionMappingEvent {
final ActionMapping mapping;
const CreateActionMappingEvent(this.mapping);
@override
List<Object?> get props => [mapping];
}
class UpdateActionMappingEvent extends ActionMappingEvent {
final ActionMapping mapping;
const UpdateActionMappingEvent(this.mapping);
@override
List<Object?> get props => [mapping];
}
class DeleteActionMappingEvent extends ActionMappingEvent {
final String id;
const DeleteActionMappingEvent(this.id);
@override
List<Object?> get props => [id];
}
class SyncActionMappingsEvent extends ActionMappingEvent {
const SyncActionMappingsEvent();
}
class DownloadActionMappingsEvent extends ActionMappingEvent {
const DownloadActionMappingsEvent();
}
class CheckDirtyCountEvent extends ActionMappingEvent {
const CheckDirtyCountEvent();
}

View File

@@ -0,0 +1,68 @@
import 'package:equatable/equatable.dart';
import '../../../domain/entities/action_mapping.dart';
abstract class ActionMappingState extends Equatable {
const ActionMappingState();
@override
List<Object?> get props => [];
}
class ActionMappingInitial extends ActionMappingState {
const ActionMappingInitial();
}
class ActionMappingLoading extends ActionMappingState {
const ActionMappingLoading();
}
class ActionMappingLoaded extends ActionMappingState {
final List<ActionMapping> mappings;
final int dirtyCount;
const ActionMappingLoaded(this.mappings, {this.dirtyCount = 0});
@override
List<Object?> get props => [mappings, dirtyCount];
}
class ActionMappingOperationSuccess extends ActionMappingState {
final String message;
const ActionMappingOperationSuccess(this.message);
@override
List<Object?> get props => [message];
}
class ActionMappingError extends ActionMappingState {
final String message;
const ActionMappingError(this.message);
@override
List<Object?> get props => [message];
}
class ActionMappingSyncing extends ActionMappingState {
final String message;
const ActionMappingSyncing({this.message = 'Syncing changes...'});
@override
List<Object?> get props => [message];
}
class ActionMappingSyncSuccess extends ActionMappingState {
final String message;
final int syncedCount;
const ActionMappingSyncSuccess(this.message, this.syncedCount);
@override
List<Object?> get props => [message, syncedCount];
}
class ActionMappingDownloading extends ActionMappingState {
const ActionMappingDownloading();
}