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:
161
geutebruck_app/lib/data/services/excel_import_service.dart
Normal file
161
geutebruck_app/lib/data/services/excel_import_service.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user