feat: GeViScope SDK integration with C# Bridge and Flutter app

- Add GeViScope Bridge (C# .NET 8.0) on port 7720
  - Full SDK wrapper for camera control, PTZ, actions/events
  - 17 REST API endpoints for GeViScope server interaction
  - Support for MCS (Media Channel Simulator) with 16 test channels
  - Real-time action/event streaming via PLC callbacks

- Add GeViServer Bridge (C# .NET 8.0) on port 7710
  - Integration with GeViSoft orchestration layer
  - Input/output control and event management

- Update Python API with new routers
  - /api/geviscope/* - Proxy to GeViScope Bridge
  - /api/geviserver/* - Proxy to GeViServer Bridge
  - /api/excel/* - Excel import functionality

- Add Flutter app GeViScope integration
  - GeViScopeRemoteDataSource with 17 API methods
  - GeViScopeBloc for state management
  - GeViScopeScreen with PTZ controls
  - App drawer navigation to GeViScope

- Add SDK documentation (extracted from PDFs)
  - GeViScope SDK docs (7 parts + action reference)
  - GeViSoft SDK docs (12 chunks)

- Add .mcp.json for Claude Code MCP server config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Administrator
2026-01-19 08:14:17 +01:00
parent c9e83e4277
commit a92b909539
76 changed files with 62101 additions and 176 deletions

View File

@@ -0,0 +1,161 @@
import 'dart:typed_data';
import 'package:dio/dio.dart';
import '../../domain/entities/server.dart';
import '../../core/constants/api_constants.dart';
import '../../core/storage/token_manager.dart';
import '../data_sources/local/server_local_data_source.dart';
import '../models/server_hive_model.dart';
import 'package:uuid/uuid.dart';
class ExcelImportService {
final _uuid = const Uuid();
final Dio _dio = Dio();
final ServerLocalDataSource? _localDataSource;
ExcelImportService({ServerLocalDataSource? localDataSource})
: _localDataSource = localDataSource;
/// Import servers from Excel file using backend API
/// Expected columns (starting from row 2):
/// - Column B: Hostname/Alias
/// - Column C: Type (GeViScope or G-Core)
/// - Column D: IP Server/Host
/// - Column E: Username
/// - Column F: Password
Future<List<Server>> importServersFromExcel(Uint8List fileBytes, String fileName) async {
try {
print('[ExcelImport] Starting import, file size: ${fileBytes.length} bytes');
// Get auth token
final token = TokenManager().accessToken;
// Prepare multipart request
final formData = FormData.fromMap({
'file': MultipartFile.fromBytes(
fileBytes,
filename: fileName,
),
});
// Call backend API
final response = await _dio.post(
'${ApiConstants.baseUrl}/excel/import-servers',
data: formData,
options: Options(
headers: {
'Authorization': 'Bearer $token',
},
),
);
if (response.statusCode != 200) {
throw Exception('Server returned status ${response.statusCode}');
}
final data = response.data as Map<String, dynamic>;
final serversData = data['servers'] as List<dynamic>;
print('[ExcelImport] Server returned ${serversData.length} servers');
// Convert API response to Server entities
final servers = <Server>[];
for (final serverData in serversData) {
final serverType = serverData['type'] == 'gcore'
? ServerType.gcore
: ServerType.geviscope;
final server = Server(
id: _uuid.v4(),
alias: serverData['alias'] as String,
host: serverData['host'] as String,
user: serverData['user'] as String? ?? 'sysadmin',
password: serverData['password'] as String? ?? '',
type: serverType,
enabled: serverData['enabled'] as bool? ?? true,
deactivateEcho: serverData['deactivateEcho'] as bool? ?? false,
deactivateLiveCheck: serverData['deactivateLiveCheck'] as bool? ?? false,
);
servers.add(server);
}
print('[ExcelImport] Import completed: ${servers.length} servers parsed');
return servers;
} catch (e) {
print('[ExcelImport] Fatal error: $e');
if (e is DioException) {
print('[ExcelImport] DioException type: ${e.type}');
print('[ExcelImport] Response status: ${e.response?.statusCode}');
print('[ExcelImport] Response data: ${e.response?.data}');
print('[ExcelImport] Request URL: ${e.requestOptions.uri}');
final errorMessage = e.response?.data?['detail'] ??
e.response?.data?['error'] ??
e.message ??
'Unknown error';
throw Exception('Failed to import Excel file: $errorMessage');
}
throw Exception('Failed to import Excel file: $e');
}
}
/// Merge imported servers with existing servers
/// Only adds servers that don't already exist (based on alias or host)
List<Server> mergeServers({
required List<Server> existing,
required List<Server> imported,
}) {
final newServers = <Server>[];
int duplicateCount = 0;
for (final importedServer in imported) {
// Check if server already exists by alias or host
final isDuplicate = existing.any((existingServer) =>
existingServer.alias.toLowerCase() == importedServer.alias.toLowerCase() ||
existingServer.host.toLowerCase() == importedServer.host.toLowerCase());
if (!isDuplicate) {
newServers.add(importedServer);
print('[ExcelImport] New server: ${importedServer.alias}');
} else {
duplicateCount++;
print('[ExcelImport] Duplicate skipped: ${importedServer.alias}');
}
}
print('[ExcelImport] Merge complete: ${newServers.length} new servers, $duplicateCount duplicates skipped');
return newServers;
}
/// Save imported servers directly to local storage as dirty (unsaved) servers
/// This bypasses the bloc to avoid triggering multiple rebuilds during import
Future<void> saveImportedServersToStorage(List<Server> servers) async {
if (_localDataSource == null) {
throw Exception('LocalDataSource not available for direct storage access');
}
print('[ExcelImport] Saving ${servers.length} servers directly to storage...');
for (final server in servers) {
final hiveModel = ServerHiveModel(
id: server.id,
alias: server.alias,
host: server.host,
user: server.user,
password: server.password,
serverType: server.type == ServerType.gcore ? 'gcore' : 'geviscope',
enabled: server.enabled,
deactivateEcho: server.deactivateEcho,
deactivateLiveCheck: server.deactivateLiveCheck,
isDirty: true, // Mark as dirty (unsaved change)
syncOperation: 'create', // Needs to be created on server
lastModified: DateTime.now(),
);
await _localDataSource!.saveServer(hiveModel);
print('[ExcelImport] Saved to storage: ${server.alias}');
}
print('[ExcelImport] All ${servers.length} servers saved to storage as unsaved changes');
}
}

View File

@@ -139,8 +139,8 @@ class SyncServiceImpl implements SyncService {
// Fetch all servers from API
final servers = await remoteDataSource.getAllServers();
// Replace local storage (preserving dirty servers)
await localDataSource.replaceAllServers(servers);
// Replace local storage with force=true to discard all local changes
await localDataSource.replaceAllServers(servers, force: true);
return Right(servers.length);
} on ServerException catch (e) {