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,141 @@
import 'package:hive_flutter/hive_flutter.dart';
import '../../models/action_mapping_hive_model.dart';
import '../../models/action_mapping_model.dart';
/// Abstract interface for local action mapping data operations
abstract class ActionMappingLocalDataSource {
Future<List<ActionMappingHiveModel>> getAllActionMappings();
Future<ActionMappingHiveModel?> getActionMappingById(String id);
Future<void> saveActionMapping(ActionMappingHiveModel mapping);
Future<void> deleteActionMapping(String id);
Future<List<ActionMappingHiveModel>> getDirtyActionMappings();
Future<void> markActionMappingAsSynced(String id);
Future<void> replaceAllActionMappings(List<ActionMappingModel> mappings, {bool force = false});
Future<List<ActionMappingHiveModel>> searchActionMappings(String query);
Future<void> clearAll();
}
/// Implementation of local action mapping data source using Hive
class ActionMappingLocalDataSourceImpl implements ActionMappingLocalDataSource {
static const String _boxName = 'action_mappings';
Future<Box<ActionMappingHiveModel>> get _box async =>
await Hive.openBox<ActionMappingHiveModel>(_boxName);
@override
Future<List<ActionMappingHiveModel>> getAllActionMappings() async {
final box = await _box;
return box.values
.where((mapping) => mapping.syncOperation != 'delete')
.toList();
}
@override
Future<ActionMappingHiveModel?> getActionMappingById(String id) async {
final box = await _box;
final mapping = box.get(id);
// Don't return deleted items
if (mapping?.syncOperation == 'delete') {
return null;
}
return mapping;
}
@override
Future<void> saveActionMapping(ActionMappingHiveModel mapping) async {
final box = await _box;
await box.put(mapping.id, mapping);
}
@override
Future<void> deleteActionMapping(String id) async {
final box = await _box;
final existing = box.get(id);
if (existing != null) {
// Soft delete: mark as dirty with delete operation
final deleted = existing.copyWith(
isDirty: true,
syncOperation: 'delete',
lastModified: DateTime.now(),
);
await box.put(id, deleted);
}
}
@override
Future<List<ActionMappingHiveModel>> getDirtyActionMappings() async {
final box = await _box;
return box.values.where((mapping) => mapping.isDirty).toList();
}
@override
Future<void> markActionMappingAsSynced(String id) async {
final box = await _box;
final mapping = box.get(id);
if (mapping != null) {
if (mapping.syncOperation == 'delete') {
// Actually delete after successful sync
await box.delete(id);
} else {
// Clear dirty flag and sync operation
final synced = mapping.copyWith(
isDirty: false,
syncOperation: null,
);
await box.put(id, synced);
}
}
}
@override
Future<void> replaceAllActionMappings(List<ActionMappingModel> mappings, {bool force = false}) async {
final box = await _box;
if (force) {
// Force mode: Clear ALL items (including dirty) to get fresh server data
await box.clear();
} else {
// Normal mode: Clear existing non-dirty items
final keysToDelete = box.values
.where((mapping) => !mapping.isDirty)
.map((mapping) => mapping.id)
.toList();
for (final key in keysToDelete) {
await box.delete(key);
}
}
// Add new items from server
for (final model in mappings) {
final hiveModel = ActionMappingHiveModel.fromActionMappingModel(
model,
isDirty: false,
);
await box.put(hiveModel.id, hiveModel);
}
}
@override
Future<List<ActionMappingHiveModel>> searchActionMappings(String query) async {
final box = await _box;
final lowerQuery = query.toLowerCase();
return box.values
.where((mapping) =>
mapping.syncOperation != 'delete' &&
(mapping.name.toLowerCase().contains(lowerQuery) ||
(mapping.description?.toLowerCase().contains(lowerQuery) ?? false)))
.toList();
}
@override
Future<void> clearAll() async {
final box = await _box;
await box.clear();
}
}

View File

@@ -0,0 +1,110 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../../../core/errors/exceptions.dart';
import '../../../core/storage/token_manager.dart';
class SecureStorageManager {
final FlutterSecureStorage storage;
SecureStorageManager({required this.storage});
// Token storage
Future<void> saveAccessToken(String token) async {
// Save to memory for immediate use
TokenManager().saveTokens(accessToken: token);
try {
await storage.write(key: 'access_token', value: token);
} catch (e) {
// Silently fail on web when storage is not available (HTTP context)
print('Warning: Failed to save access token to secure storage (using memory): $e');
}
}
Future<void> saveRefreshToken(String token) async {
// Save to memory for immediate use
TokenManager().saveTokens(refreshToken: token);
try {
await storage.write(key: 'refresh_token', value: token);
} catch (e) {
// Silently fail on web when storage is not available (HTTP context)
print('Warning: Failed to save refresh token to secure storage (using memory): $e');
}
}
Future<String?> getAccessToken() async {
try {
final token = await storage.read(key: 'access_token');
if (token != null) return token;
} catch (e) {
print('Warning: Failed to read access token from secure storage, using memory');
}
// Fallback to memory storage
return TokenManager().accessToken;
}
Future<String?> getRefreshToken() async {
try {
final token = await storage.read(key: 'refresh_token');
if (token != null) return token;
} catch (e) {
print('Warning: Failed to read refresh token from secure storage, using memory');
}
// Fallback to memory storage
return TokenManager().refreshToken;
}
// User data storage
Future<void> saveUsername(String username) async {
// Save to memory for immediate use
TokenManager().saveTokens(username: username);
try {
await storage.write(key: 'username', value: username);
} catch (e) {
// Silently fail on web when storage is not available (HTTP context)
print('Warning: Failed to save username to secure storage (using memory): $e');
}
}
Future<void> saveUserRole(String role) async {
// Save to memory for immediate use
TokenManager().saveTokens(userRole: role);
try {
await storage.write(key: 'user_role', value: role);
} catch (e) {
// Silently fail on web when storage is not available (HTTP context)
print('Warning: Failed to save user role to secure storage (using memory): $e');
}
}
Future<String?> getUsername() async {
try {
return await storage.read(key: 'username');
} catch (e) {
throw CacheException('Failed to read username');
}
}
Future<String?> getUserRole() async {
try {
return await storage.read(key: 'user_role');
} catch (e) {
throw CacheException('Failed to read user role');
}
}
// Clear all data
Future<void> clearAll() async {
// Clear memory storage
TokenManager().clear();
try {
await storage.deleteAll();
} catch (e) {
print('Warning: Failed to clear secure storage: $e');
// Not throwing exception since memory is already cleared
}
}
}

View File

@@ -0,0 +1,159 @@
import 'package:hive/hive.dart';
import '../../models/server_hive_model.dart';
import '../../models/server_model.dart';
abstract class ServerLocalDataSource {
/// Get all servers from local storage
Future<List<ServerHiveModel>> getAllServers();
/// Get servers by type
Future<List<ServerHiveModel>> getServersByType(String type);
/// Get server by ID and type
Future<ServerHiveModel?> getServerById(String id, String type);
/// Save server to local storage (create or update)
Future<void> saveServer(ServerHiveModel server);
/// Delete server from local storage
Future<void> deleteServer(String id, String type);
/// Get all servers with pending sync operations
Future<List<ServerHiveModel>> getDirtyServers();
/// Clear dirty flag after successful sync
Future<void> markServerAsSynced(String id, String type);
/// Replace all servers (used after fetching from API)
Future<void> replaceAllServers(List<ServerModel> servers);
/// Clear all local data
Future<void> clearAll();
}
class ServerLocalDataSourceImpl implements ServerLocalDataSource {
static const String _boxName = 'servers';
Box<ServerHiveModel>? _box;
Future<Box<ServerHiveModel>> get box async {
if (_box != null && _box!.isOpen) {
return _box!;
}
_box = await Hive.openBox<ServerHiveModel>(_boxName);
return _box!;
}
String _getKey(String id, String type) => '${type}_$id';
@override
Future<List<ServerHiveModel>> getAllServers() async {
final b = await box;
return b.values.where((s) => s.syncOperation != 'delete').toList();
}
@override
Future<List<ServerHiveModel>> getServersByType(String type) async {
final b = await box;
return b.values
.where((s) => s.serverType == type && s.syncOperation != 'delete')
.toList();
}
@override
Future<ServerHiveModel?> getServerById(String id, String type) async {
final b = await box;
final key = _getKey(id, type);
final server = b.get(key);
// Don't return deleted servers
if (server?.syncOperation == 'delete') {
return null;
}
return server;
}
@override
Future<void> saveServer(ServerHiveModel server) async {
final b = await box;
final key = _getKey(server.id, server.serverType);
await b.put(key, server);
}
@override
Future<void> deleteServer(String id, String type) async {
final b = await box;
final key = _getKey(id, type);
final existing = b.get(key);
if (existing != null) {
// Mark as deleted instead of actually deleting
// This allows us to sync the deletion to the server
final deleted = existing.copyWith(
isDirty: true,
syncOperation: 'delete',
lastModified: DateTime.now(),
);
await b.put(key, deleted);
}
}
@override
Future<List<ServerHiveModel>> getDirtyServers() async {
final b = await box;
return b.values.where((s) => s.isDirty).toList();
}
@override
Future<void> markServerAsSynced(String id, String type) async {
final b = await box;
final key = _getKey(id, type);
final server = b.get(key);
if (server != null) {
if (server.syncOperation == 'delete') {
// Actually delete after successful sync
await b.delete(key);
} else {
// Clear dirty flag
final synced = server.copyWith(
isDirty: false,
syncOperation: null,
);
await b.put(key, synced);
}
}
}
@override
Future<void> replaceAllServers(List<ServerModel> servers) async {
final b = await box;
// Don't clear dirty servers - keep them for sync
final dirtyServers = await getDirtyServers();
final dirtyKeys = dirtyServers.map((s) => _getKey(s.id, s.serverType)).toSet();
// Clear only non-dirty servers
await b.clear();
// Re-add dirty servers
for (final dirty in dirtyServers) {
await b.put(_getKey(dirty.id, dirty.serverType), dirty);
}
// Add all fetched servers (but don't overwrite dirty ones)
for (final server in servers) {
final key = _getKey(server.id, server.serverType);
if (!dirtyKeys.contains(key)) {
await b.put(key, ServerHiveModel.fromServerModel(server));
}
}
}
@override
Future<void> clearAll() async {
final b = await box;
await b.clear();
}
}

View File

@@ -0,0 +1,110 @@
import 'package:dio/dio.dart';
import '../../../core/constants/api_constants.dart';
import '../../../core/errors/exceptions.dart';
import '../../models/action_mapping_model.dart';
/// Abstract interface for remote action mapping data operations
abstract class ActionMappingRemoteDataSource {
Future<List<ActionMappingModel>> getAllActionMappings();
Future<ActionMappingModel> getActionMappingById(String id);
Future<void> createActionMapping(ActionMappingModel mapping);
Future<void> updateActionMapping(String id, ActionMappingModel mapping);
Future<void> deleteActionMapping(String id);
}
/// Implementation of remote action mapping data source using Dio
class ActionMappingRemoteDataSourceImpl implements ActionMappingRemoteDataSource {
final Dio dio;
ActionMappingRemoteDataSourceImpl({required this.dio});
@override
Future<List<ActionMappingModel>> getAllActionMappings() async {
try {
final response = await dio.get(ApiConstants.actionMappingsEndpoint);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
final List<dynamic> mappingsList = data['mappings'] as List<dynamic>;
return mappingsList.map((json) => ActionMappingModel.fromJson(json)).toList();
} else {
throw ActionMappingException('Failed to fetch action mappings');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
@override
Future<ActionMappingModel> getActionMappingById(String id) async {
try {
final response = await dio.get('${ApiConstants.actionMappingsEndpoint}/$id');
if (response.statusCode == 200) {
return ActionMappingModel.fromJson(response.data);
} else {
throw ActionMappingException('Failed to fetch action mapping');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
@override
Future<void> createActionMapping(ActionMappingModel mapping) async {
try {
final response = await dio.post(
ApiConstants.actionMappingsEndpoint,
data: mapping.toJson(),
);
if (response.statusCode != 200 && response.statusCode != 201) {
throw ActionMappingException('Failed to create action mapping');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
@override
Future<void> updateActionMapping(String id, ActionMappingModel mapping) async {
try {
final response = await dio.put(
'${ApiConstants.actionMappingsEndpoint}/$id',
data: mapping.toJson(),
);
if (response.statusCode != 200) {
throw ActionMappingException('Failed to update action mapping');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
@override
Future<void> deleteActionMapping(String id) async {
try {
final response = await dio.delete('${ApiConstants.actionMappingsEndpoint}/$id');
if (response.statusCode != 200 && response.statusCode != 204) {
throw ActionMappingException('Failed to delete action mapping');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
Exception _handleDioException(DioException e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
return NetworkException('Connection timeout');
} else if (e.type == DioExceptionType.connectionError) {
return NetworkException('No internet connection');
} else if (e.response?.statusCode == 401) {
return AuthenticationException('Unauthorized');
} else {
return ActionMappingException('Server error: ${e.message}');
}
}
}

View File

@@ -0,0 +1,54 @@
import 'package:dio/dio.dart';
import '../../../core/constants/api_constants.dart';
import '../../../core/errors/exceptions.dart';
import '../../models/auth_model.dart';
abstract class AuthRemoteDataSource {
Future<AuthResponseModel> login(String username, String password);
Future<void> logout();
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final Dio dio;
AuthRemoteDataSourceImpl({required this.dio});
@override
Future<AuthResponseModel> login(String username, String password) async {
try {
final response = await dio.post(
ApiConstants.loginEndpoint,
data: {
'username': username,
'password': password,
},
);
if (response.statusCode == 200) {
return AuthResponseModel.fromJson(response.data);
} else {
throw ServerException('Login failed: ${response.statusMessage}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw AuthenticationException('Invalid credentials');
} else if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
throw NetworkException('Connection timeout');
} else if (e.type == DioExceptionType.connectionError) {
throw NetworkException('No internet connection');
} else {
throw ServerException('Server error: ${e.message}');
}
} catch (e) {
throw ServerException('Unexpected error: $e');
}
}
@override
Future<void> logout() async {
// In this implementation, logout is handled locally (clearing tokens)
// If the API has a logout endpoint, implement it here
return;
}
}

View File

@@ -0,0 +1,33 @@
import '../../../core/errors/exceptions.dart';
import '../../models/auth_model.dart';
import 'auth_remote_data_source.dart';
/// Mock implementation of AuthRemoteDataSource for development/testing
/// Accepts hardcoded credentials: admin/admin123
class AuthRemoteDataSourceMock implements AuthRemoteDataSource {
@override
Future<AuthResponseModel> login(String username, String password) async {
// Simulate network delay
await Future.delayed(const Duration(milliseconds: 500));
// Check credentials
if (username == 'admin' && password == 'admin123') {
// Return mock successful auth response
return const AuthResponseModel(
accessToken: 'mock_access_token_12345',
refreshToken: 'mock_refresh_token_67890',
username: 'admin',
role: 'Administrator',
);
} else {
// Invalid credentials
throw AuthenticationException('Invalid username or password');
}
}
@override
Future<void> logout() async {
// Mock logout - nothing to do
await Future.delayed(const Duration(milliseconds: 200));
}
}

View File

@@ -0,0 +1,177 @@
import 'package:dio/dio.dart';
import '../../../core/constants/api_constants.dart';
import '../../../core/errors/exceptions.dart';
import '../../models/server_model.dart';
abstract class ServerRemoteDataSource {
Future<List<ServerModel>> getAllServers();
Future<List<ServerModel>> getGCoreServers();
Future<List<ServerModel>> getGeViScopeServers();
Future<ServerModel> getServerById(String id, String type);
Future<void> createServer(ServerModel server);
Future<void> updateServer(ServerModel server);
Future<void> deleteServer(String id, String type);
}
class ServerRemoteDataSourceImpl implements ServerRemoteDataSource {
final Dio dio;
ServerRemoteDataSourceImpl({required this.dio});
@override
Future<List<ServerModel>> getAllServers() async {
try {
final response = await dio.get(ApiConstants.serversEndpoint);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
final List<ServerModel> servers = [];
// Parse gcore servers
if (data['gcore_servers'] != null) {
final gcoreList = data['gcore_servers'] as List<dynamic>;
servers.addAll(
gcoreList.map((json) => ServerModel.fromJson(json, 'gcore')),
);
}
// Parse geviscope servers
if (data['geviscope_servers'] != null) {
final geviscopeList = data['geviscope_servers'] as List<dynamic>;
servers.addAll(
geviscopeList.map((json) => ServerModel.fromJson(json, 'geviscope')),
);
}
return servers;
} else {
throw ServerException('Failed to fetch servers');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
@override
Future<List<ServerModel>> getGCoreServers() async {
try {
final response = await dio.get(ApiConstants.gcoreServersEndpoint);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
final List<dynamic> serversList = data['servers'] as List<dynamic>;
return serversList.map((json) => ServerModel.fromJson(json, 'gcore')).toList();
} else {
throw ServerException('Failed to fetch G-Core servers');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
@override
Future<List<ServerModel>> getGeViScopeServers() async {
try {
final response = await dio.get(ApiConstants.geviscopeServersEndpoint);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
final List<dynamic> serversList = data['servers'] as List<dynamic>;
return serversList.map((json) => ServerModel.fromJson(json, 'geviscope')).toList();
} else {
throw ServerException('Failed to fetch GeViScope servers');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
@override
Future<ServerModel> getServerById(String id, String type) async {
try {
final endpoint = type == 'gcore'
? '${ApiConstants.gcoreServersEndpoint}/$id'
: '${ApiConstants.geviscopeServersEndpoint}/$id';
final response = await dio.get(endpoint);
if (response.statusCode == 200) {
return ServerModel.fromJson(response.data, type);
} else {
throw ServerException('Failed to fetch server');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
@override
Future<void> createServer(ServerModel server) async {
try {
final endpoint = server.serverType == 'gcore'
? ApiConstants.gcoreServersEndpoint
: ApiConstants.geviscopeServersEndpoint;
final response = await dio.post(
endpoint,
data: server.toJson(),
);
if (response.statusCode != 200 && response.statusCode != 201) {
throw ServerException('Failed to create server');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
@override
Future<void> updateServer(ServerModel server) async {
try {
final endpoint = server.serverType == 'gcore'
? '${ApiConstants.gcoreServersEndpoint}/${server.id}'
: '${ApiConstants.geviscopeServersEndpoint}/${server.id}';
final response = await dio.put(
endpoint,
data: server.toJson(),
);
if (response.statusCode != 200) {
throw ServerException('Failed to update server');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
@override
Future<void> deleteServer(String id, String type) async {
try {
final endpoint = type == 'gcore'
? '${ApiConstants.gcoreServersEndpoint}/$id'
: '${ApiConstants.geviscopeServersEndpoint}/$id';
final response = await dio.delete(endpoint);
if (response.statusCode != 200 && response.statusCode != 204) {
throw ServerException('Failed to delete server');
}
} on DioException catch (e) {
throw _handleDioException(e);
}
}
Exception _handleDioException(DioException e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
return NetworkException('Connection timeout');
} else if (e.type == DioExceptionType.connectionError) {
return NetworkException('No internet connection');
} else if (e.response?.statusCode == 401) {
return AuthenticationException('Unauthorized');
} else {
return ServerException('Server error: ${e.message}');
}
}
}

View File

@@ -0,0 +1,229 @@
import 'dart:convert';
import 'package:hive/hive.dart';
import '../../domain/entities/action_mapping.dart';
import 'action_mapping_model.dart';
import 'action_output.dart';
part 'action_mapping_hive_model.g.dart';
/// Hive model for local storage of action mappings with sync tracking
@HiveType(typeId: 1)
class ActionMappingHiveModel extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String name;
@HiveField(2)
final String? description;
@HiveField(3)
final String inputAction;
@HiveField(4)
final List<String> outputActions; // Deprecated - kept for compatibility
@HiveField(5)
final String? geviscopeInstanceScope;
@HiveField(6)
final bool enabled;
@HiveField(7)
final int executionCount;
@HiveField(8)
final DateTime? lastExecuted;
@HiveField(9)
final DateTime createdAt;
@HiveField(10)
final DateTime updatedAt;
@HiveField(11)
final String createdBy;
// Sync tracking fields
@HiveField(12)
final bool isDirty;
@HiveField(13)
final DateTime lastModified;
@HiveField(14)
final String? syncOperation; // 'create', 'update', 'delete'
// New fields for parameters (stored as JSON strings)
@HiveField(15)
final String? inputParametersJson; // JSON-encoded Map<String, dynamic>
@HiveField(16)
final String? outputActionsJson; // JSON-encoded List<ActionOutput>
ActionMappingHiveModel({
required this.id,
required this.name,
this.description,
required this.inputAction,
required this.outputActions,
this.geviscopeInstanceScope,
this.enabled = true,
this.executionCount = 0,
this.lastExecuted,
required this.createdAt,
required this.updatedAt,
required this.createdBy,
this.isDirty = false,
required this.lastModified,
this.syncOperation,
this.inputParametersJson,
this.outputActionsJson,
});
/// Create Hive model from domain entity
factory ActionMappingHiveModel.fromEntity(
ActionMapping mapping, {
bool isDirty = false,
String? syncOperation,
}) {
return ActionMappingHiveModel(
id: mapping.id,
name: mapping.name,
description: mapping.description,
inputAction: mapping.inputAction,
outputActions: mapping.outputActions.map((o) => o.action).toList(), // For compatibility
geviscopeInstanceScope: mapping.geviscopeInstanceScope,
enabled: mapping.enabled,
executionCount: mapping.executionCount,
lastExecuted: mapping.lastExecuted,
createdAt: mapping.createdAt,
updatedAt: mapping.updatedAt,
createdBy: mapping.createdBy,
isDirty: isDirty,
lastModified: DateTime.now(),
syncOperation: syncOperation,
inputParametersJson: jsonEncode(mapping.inputParameters),
outputActionsJson: jsonEncode(mapping.outputActions.map((o) => o.toJson()).toList()),
);
}
/// Create Hive model from API model
factory ActionMappingHiveModel.fromActionMappingModel(
ActionMappingModel model, {
bool isDirty = false,
String? syncOperation,
}) {
return ActionMappingHiveModel(
id: model.id,
name: model.name,
description: model.description,
inputAction: model.inputAction,
outputActions: model.outputActions.map((o) => o.action).toList(), // For compatibility
geviscopeInstanceScope: model.geviscopeInstanceScope,
enabled: model.enabled,
executionCount: model.executionCount,
lastExecuted: model.lastExecuted,
createdAt: model.createdAt,
updatedAt: model.updatedAt,
createdBy: model.createdBy,
isDirty: isDirty,
lastModified: DateTime.now(),
syncOperation: syncOperation,
inputParametersJson: jsonEncode(model.inputParameters),
outputActionsJson: jsonEncode(model.outputActions.map((o) => o.toJson()).toList()),
);
}
/// Convert to API model
ActionMappingModel toActionMappingModel() {
// Decode parameters from JSON
Map<String, dynamic> inputParams = {};
List<ActionOutput> outputs = [];
if (inputParametersJson != null && inputParametersJson!.isNotEmpty) {
try {
inputParams = Map<String, dynamic>.from(jsonDecode(inputParametersJson!));
} catch (e) {
print('Error decoding inputParametersJson: $e');
}
}
if (outputActionsJson != null && outputActionsJson!.isNotEmpty) {
try {
final decoded = jsonDecode(outputActionsJson!) as List<dynamic>;
outputs = decoded.map((e) => ActionOutput.fromJson(e as Map<String, dynamic>)).toList();
} catch (e) {
print('Error decoding outputActionsJson: $e');
// Fallback to legacy outputActions
outputs = outputActions.map((action) => ActionOutput(action: action, parameters: {})).toList();
}
} else {
// Fallback to legacy outputActions
outputs = outputActions.map((action) => ActionOutput(action: action, parameters: {})).toList();
}
return ActionMappingModel(
id: id,
name: name,
description: description,
inputAction: inputAction,
inputParameters: inputParams,
outputActions: outputs,
geviscopeInstanceScope: geviscopeInstanceScope,
enabled: enabled,
executionCount: executionCount,
lastExecuted: lastExecuted,
createdAt: createdAt,
updatedAt: updatedAt,
createdBy: createdBy,
);
}
/// Convert to domain entity
ActionMapping toEntity() {
return toActionMappingModel().toEntity();
}
/// Create a copy with modified fields
ActionMappingHiveModel copyWith({
String? id,
String? name,
String? description,
String? inputAction,
List<String>? outputActions,
String? geviscopeInstanceScope,
bool? enabled,
int? executionCount,
DateTime? lastExecuted,
DateTime? createdAt,
DateTime? updatedAt,
String? createdBy,
bool? isDirty,
DateTime? lastModified,
String? syncOperation,
String? inputParametersJson,
String? outputActionsJson,
}) {
return ActionMappingHiveModel(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
inputAction: inputAction ?? this.inputAction,
outputActions: outputActions ?? this.outputActions,
geviscopeInstanceScope: geviscopeInstanceScope ?? this.geviscopeInstanceScope,
enabled: enabled ?? this.enabled,
executionCount: executionCount ?? this.executionCount,
lastExecuted: lastExecuted ?? this.lastExecuted,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
createdBy: createdBy ?? this.createdBy,
isDirty: isDirty ?? this.isDirty,
lastModified: lastModified ?? this.lastModified,
syncOperation: syncOperation ?? this.syncOperation,
inputParametersJson: inputParametersJson ?? this.inputParametersJson,
outputActionsJson: outputActionsJson ?? this.outputActionsJson,
);
}
}

View File

@@ -0,0 +1,90 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'action_mapping_hive_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ActionMappingHiveModelAdapter
extends TypeAdapter<ActionMappingHiveModel> {
@override
final int typeId = 1;
@override
ActionMappingHiveModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ActionMappingHiveModel(
id: fields[0] as String,
name: fields[1] as String,
description: fields[2] as String?,
inputAction: fields[3] as String,
outputActions: (fields[4] as List).cast<String>(),
geviscopeInstanceScope: fields[5] as String?,
enabled: fields[6] as bool,
executionCount: fields[7] as int,
lastExecuted: fields[8] as DateTime?,
createdAt: fields[9] as DateTime,
updatedAt: fields[10] as DateTime,
createdBy: fields[11] as String,
isDirty: fields[12] as bool,
lastModified: fields[13] as DateTime,
syncOperation: fields[14] as String?,
inputParametersJson: fields[15] as String?,
outputActionsJson: fields[16] as String?,
);
}
@override
void write(BinaryWriter writer, ActionMappingHiveModel obj) {
writer
..writeByte(17)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.description)
..writeByte(3)
..write(obj.inputAction)
..writeByte(4)
..write(obj.outputActions)
..writeByte(5)
..write(obj.geviscopeInstanceScope)
..writeByte(6)
..write(obj.enabled)
..writeByte(7)
..write(obj.executionCount)
..writeByte(8)
..write(obj.lastExecuted)
..writeByte(9)
..write(obj.createdAt)
..writeByte(10)
..write(obj.updatedAt)
..writeByte(11)
..write(obj.createdBy)
..writeByte(12)
..write(obj.isDirty)
..writeByte(13)
..write(obj.lastModified)
..writeByte(14)
..write(obj.syncOperation)
..writeByte(15)
..write(obj.inputParametersJson)
..writeByte(16)
..write(obj.outputActionsJson);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ActionMappingHiveModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,145 @@
import '../../domain/entities/action_mapping.dart';
import 'action_output.dart';
/// Data model for action mapping with JSON serialization
/// Handles conversion between API (snake_case) and domain entities
class ActionMappingModel {
final String id;
final String name;
final String? description;
final String inputAction;
final Map<String, dynamic> inputParameters;
final List<ActionOutput> outputActions;
final String? geviscopeInstanceScope;
final bool enabled;
final int executionCount;
final DateTime? lastExecuted;
final DateTime createdAt;
final DateTime updatedAt;
final String createdBy;
const ActionMappingModel({
required this.id,
required this.name,
this.description,
required this.inputAction,
this.inputParameters = const {},
required this.outputActions,
this.geviscopeInstanceScope,
this.enabled = true,
this.executionCount = 0,
this.lastExecuted,
required this.createdAt,
required this.updatedAt,
required this.createdBy,
});
/// Create model from JSON (API response with snake_case keys)
factory ActionMappingModel.fromJson(Map<String, dynamic> json) {
// Convert ID from int to string if needed
final id = json['id'].toString();
// Extract input action and parameters from array of objects
final inputActions = json['input_actions'] as List<dynamic>?;
String inputAction;
Map<String, dynamic> inputParameters;
if (inputActions != null && inputActions.isNotEmpty) {
final firstInput = inputActions[0] as Map<String, dynamic>;
inputAction = firstInput['action'] as String;
inputParameters = Map<String, dynamic>.from(firstInput['parameters'] as Map? ?? {});
} else {
inputAction = json['name'] as String; // Fallback to name
inputParameters = {};
}
// Extract output actions and parameters from array of objects
final outputActionsRaw = json['output_actions'] as List<dynamic>?;
final outputActions = outputActionsRaw != null
? outputActionsRaw.map((e) => ActionOutput.fromJson(e as Map<String, dynamic>)).toList()
: <ActionOutput>[];
return ActionMappingModel(
id: id,
name: json['name'] as String,
description: json['description'] as String?,
inputAction: inputAction,
inputParameters: inputParameters,
outputActions: outputActions,
geviscopeInstanceScope: json['geviscope_instance_scope'] as String?,
enabled: json['enabled'] as bool? ?? true,
executionCount: json['execution_count'] as int? ?? 0,
lastExecuted: json['last_executed'] != null
? DateTime.parse(json['last_executed'] as String)
: null,
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'] as String)
: DateTime.now(),
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'] as String)
: DateTime.now(),
createdBy: json['created_by'] as String? ?? 'system',
);
}
/// Convert model to JSON (for API requests with snake_case keys)
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
if (description != null) 'description': description,
// API expects input_actions as array of objects with parameters
'input_actions': [
{'action': inputAction, 'parameters': inputParameters}
],
// API expects output_actions as array of objects with parameters
'output_actions': outputActions.map((output) => output.toJson()).toList(),
if (geviscopeInstanceScope != null)
'geviscope_instance_scope': geviscopeInstanceScope,
'enabled': enabled,
'execution_count': executionCount,
if (lastExecuted != null) 'last_executed': lastExecuted!.toIso8601String(),
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'created_by': createdBy,
};
}
/// Convert to domain entity
ActionMapping toEntity() {
return ActionMapping(
id: id,
name: name,
description: description,
inputAction: inputAction,
inputParameters: inputParameters,
outputActions: outputActions,
geviscopeInstanceScope: geviscopeInstanceScope,
enabled: enabled,
executionCount: executionCount,
lastExecuted: lastExecuted,
createdAt: createdAt,
updatedAt: updatedAt,
createdBy: createdBy,
);
}
/// Create model from domain entity
factory ActionMappingModel.fromEntity(ActionMapping mapping) {
return ActionMappingModel(
id: mapping.id,
name: mapping.name,
description: mapping.description,
inputAction: mapping.inputAction,
inputParameters: mapping.inputParameters,
outputActions: mapping.outputActions,
geviscopeInstanceScope: mapping.geviscopeInstanceScope,
enabled: mapping.enabled,
executionCount: mapping.executionCount,
lastExecuted: mapping.lastExecuted,
createdAt: mapping.createdAt,
updatedAt: mapping.updatedAt,
createdBy: mapping.createdBy,
);
}
}

View File

@@ -0,0 +1,56 @@
/// Represents an output action with its parameters
class ActionOutput {
final String action;
final Map<String, dynamic> parameters;
const ActionOutput({
required this.action,
required this.parameters,
});
/// Create from JSON
factory ActionOutput.fromJson(Map<String, dynamic> json) {
return ActionOutput(
action: json['action'] as String,
parameters: Map<String, dynamic>.from(json['parameters'] as Map? ?? {}),
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'action': action,
'parameters': parameters,
};
}
/// Create a copy with updated values
ActionOutput copyWith({
String? action,
Map<String, dynamic>? parameters,
}) {
return ActionOutput(
action: action ?? this.action,
parameters: parameters ?? this.parameters,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ActionOutput &&
other.action == action &&
_mapsEqual(other.parameters, parameters);
}
@override
int get hashCode => action.hashCode ^ parameters.hashCode;
bool _mapsEqual(Map<String, dynamic> a, Map<String, dynamic> b) {
if (a.length != b.length) return false;
for (var key in a.keys) {
if (!b.containsKey(key) || a[key] != b[key]) return false;
}
return true;
}
}

View File

@@ -0,0 +1,174 @@
import 'package:equatable/equatable.dart';
/// Model representing an action template from the API
/// Used for dynamic form generation when selecting action types
class ActionTemplate extends Equatable {
final String actionName;
final List<String> parameters;
final String description;
final String category;
final bool requiredCaption;
final bool supportsDelay;
final Map<String, String>? parameterTypes;
const ActionTemplate({
required this.actionName,
required this.parameters,
required this.description,
required this.category,
this.requiredCaption = true,
this.supportsDelay = true,
this.parameterTypes,
});
factory ActionTemplate.fromJson(Map<String, dynamic> json) {
return ActionTemplate(
actionName: json['action_name'] as String,
parameters: (json['parameters'] as List<dynamic>)
.map((e) => e.toString())
.toList(),
description: json['description'] as String,
category: json['category'] as String,
requiredCaption: json['required_caption'] as bool? ?? true,
supportsDelay: json['supports_delay'] as bool? ?? true,
parameterTypes: json['parameter_types'] != null
? Map<String, String>.from(json['parameter_types'] as Map)
: null,
);
}
Map<String, dynamic> toJson() {
return {
'action_name': actionName,
'parameters': parameters,
'description': description,
'category': category,
'required_caption': requiredCaption,
'supports_delay': supportsDelay,
if (parameterTypes != null) 'parameter_types': parameterTypes,
};
}
@override
List<Object?> get props => [
actionName,
parameters,
description,
category,
requiredCaption,
supportsDelay,
parameterTypes,
];
}
/// Model representing a server (G-Core or GeViScope)
class ServerInfo extends Equatable {
final String id;
final String alias;
final bool enabled;
const ServerInfo({
required this.id,
required this.alias,
required this.enabled,
});
factory ServerInfo.fromJson(Map<String, dynamic> json) {
return ServerInfo(
id: json['id'].toString(),
alias: json['alias'] as String,
enabled: json['enabled'] as bool,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'alias': alias,
'enabled': enabled,
};
}
@override
List<Object?> get props => [id, alias, enabled];
}
/// Model for servers information
class ServersInfo extends Equatable {
final List<ServerInfo> gcoreServers;
final List<ServerInfo> gscServers;
const ServersInfo({
required this.gcoreServers,
required this.gscServers,
});
factory ServersInfo.fromJson(Map<String, dynamic> json) {
return ServersInfo(
gcoreServers: (json['gcore_servers'] as List<dynamic>?)
?.map((e) => ServerInfo.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
gscServers: (json['gsc_servers'] as List<dynamic>?)
?.map((e) => ServerInfo.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
@override
List<Object?> get props => [gcoreServers, gscServers];
}
/// Model for action categories response
class ActionCategoriesResponse extends Equatable {
final Map<String, List<String>> categories;
final int totalCategories;
final int totalActions;
final ServersInfo servers;
final List<String> gscSpecificCategories;
const ActionCategoriesResponse({
required this.categories,
required this.totalCategories,
required this.totalActions,
required this.servers,
this.gscSpecificCategories = const [],
});
factory ActionCategoriesResponse.fromJson(Map<String, dynamic> json) {
final categoriesMap = json['categories'] as Map<String, dynamic>;
final categories = categoriesMap.map(
(key, value) => MapEntry(
key,
(value as List<dynamic>).map((e) => e.toString()).toList(),
),
);
return ActionCategoriesResponse(
categories: categories,
totalCategories: json['total_categories'] as int,
totalActions: json['total_actions'] as int,
servers: ServersInfo.fromJson(json['servers'] as Map<String, dynamic>? ?? {}),
gscSpecificCategories: (json['gsc_specific_categories'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
);
}
/// Get all action names sorted by category
List<String> getActionsForCategory(String category) {
return categories[category] ?? [];
}
/// Get list of all category names sorted
List<String> get categoryNames {
final names = categories.keys.toList();
names.sort();
return names;
}
@override
List<Object?> get props => [categories, totalCategories, totalActions, servers, gscSpecificCategories];
}

View File

@@ -0,0 +1,43 @@
import '../../domain/entities/user.dart';
class AuthResponseModel {
final String accessToken;
final String refreshToken;
final String username;
final String role;
AuthResponseModel({
required this.accessToken,
required this.refreshToken,
required this.username,
required this.role,
});
factory AuthResponseModel.fromJson(Map<String, dynamic> json) {
final user = json['user'] as Map<String, dynamic>;
return AuthResponseModel(
accessToken: json['access_token'] as String,
refreshToken: json['refresh_token'] as String,
username: user['username'] as String,
role: user['role'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'access_token': accessToken,
'refresh_token': refreshToken,
'username': username,
'role': role,
};
}
User toEntity() {
return User(
username: username,
role: role,
accessToken: accessToken,
refreshToken: refreshToken,
);
}
}

View File

@@ -0,0 +1,154 @@
import 'package:hive/hive.dart';
import '../../domain/entities/server.dart';
import 'server_model.dart';
part 'server_hive_model.g.dart';
@HiveType(typeId: 0)
class ServerHiveModel extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String alias;
@HiveField(2)
final String host;
@HiveField(3)
final String user;
@HiveField(4)
final String password;
@HiveField(5)
final bool enabled;
@HiveField(6)
final bool deactivateEcho;
@HiveField(7)
final bool deactivateLiveCheck;
@HiveField(8)
final String serverType; // 'gcore' or 'geviscope'
@HiveField(9)
final bool isDirty; // Has unsaved changes
@HiveField(10)
final DateTime lastModified;
@HiveField(11)
final String? syncOperation; // 'create', 'update', 'delete', null
ServerHiveModel({
required this.id,
required this.alias,
required this.host,
required this.user,
required this.password,
required this.enabled,
required this.deactivateEcho,
required this.deactivateLiveCheck,
required this.serverType,
this.isDirty = false,
DateTime? lastModified,
this.syncOperation,
}) : lastModified = lastModified ?? DateTime.now();
// Convert from domain entity to Hive model
factory ServerHiveModel.fromEntity(Server server, {bool isDirty = false, String? syncOperation}) {
return ServerHiveModel(
id: server.id,
alias: server.alias,
host: server.host,
user: server.user,
password: server.password,
enabled: server.enabled,
deactivateEcho: server.deactivateEcho,
deactivateLiveCheck: server.deactivateLiveCheck,
serverType: server.type == ServerType.gcore ? 'gcore' : 'geviscope',
isDirty: isDirty,
syncOperation: syncOperation,
);
}
// Convert from ServerModel (API) to Hive model
factory ServerHiveModel.fromServerModel(ServerModel serverModel) {
return ServerHiveModel(
id: serverModel.id,
alias: serverModel.alias,
host: serverModel.host,
user: serverModel.user,
password: serverModel.password,
enabled: serverModel.enabled,
deactivateEcho: serverModel.deactivateEcho,
deactivateLiveCheck: serverModel.deactivateLiveCheck,
serverType: serverModel.serverType,
isDirty: false,
syncOperation: null,
);
}
// Convert to domain entity
Server toEntity() {
return Server(
id: id,
alias: alias,
host: host,
user: user,
password: password,
enabled: enabled,
deactivateEcho: deactivateEcho,
deactivateLiveCheck: deactivateLiveCheck,
type: serverType == 'gcore' ? ServerType.gcore : ServerType.geviscope,
);
}
// Convert to ServerModel for API calls
ServerModel toServerModel() {
return ServerModel(
id: id,
alias: alias,
host: host,
user: user,
password: password,
enabled: enabled,
deactivateEcho: deactivateEcho,
deactivateLiveCheck: deactivateLiveCheck,
serverType: serverType,
);
}
// Create a copy with modified fields
ServerHiveModel copyWith({
String? id,
String? alias,
String? host,
String? user,
String? password,
bool? enabled,
bool? deactivateEcho,
bool? deactivateLiveCheck,
String? serverType,
bool? isDirty,
DateTime? lastModified,
String? syncOperation,
}) {
return ServerHiveModel(
id: id ?? this.id,
alias: alias ?? this.alias,
host: host ?? this.host,
user: user ?? this.user,
password: password ?? this.password,
enabled: enabled ?? this.enabled,
deactivateEcho: deactivateEcho ?? this.deactivateEcho,
deactivateLiveCheck: deactivateLiveCheck ?? this.deactivateLiveCheck,
serverType: serverType ?? this.serverType,
isDirty: isDirty ?? this.isDirty,
lastModified: lastModified ?? this.lastModified,
syncOperation: syncOperation ?? this.syncOperation,
);
}
}

View File

@@ -0,0 +1,74 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'server_hive_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ServerHiveModelAdapter extends TypeAdapter<ServerHiveModel> {
@override
final int typeId = 0;
@override
ServerHiveModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ServerHiveModel(
id: fields[0] as String,
alias: fields[1] as String,
host: fields[2] as String,
user: fields[3] as String,
password: fields[4] as String,
enabled: fields[5] as bool,
deactivateEcho: fields[6] as bool,
deactivateLiveCheck: fields[7] as bool,
serverType: fields[8] as String,
isDirty: fields[9] as bool,
lastModified: fields[10] as DateTime?,
syncOperation: fields[11] as String?,
);
}
@override
void write(BinaryWriter writer, ServerHiveModel obj) {
writer
..writeByte(12)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.alias)
..writeByte(2)
..write(obj.host)
..writeByte(3)
..write(obj.user)
..writeByte(4)
..write(obj.password)
..writeByte(5)
..write(obj.enabled)
..writeByte(6)
..write(obj.deactivateEcho)
..writeByte(7)
..write(obj.deactivateLiveCheck)
..writeByte(8)
..write(obj.serverType)
..writeByte(9)
..write(obj.isDirty)
..writeByte(10)
..write(obj.lastModified)
..writeByte(11)
..write(obj.syncOperation);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ServerHiveModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,80 @@
import '../../domain/entities/server.dart';
class ServerModel {
final String id;
final String alias;
final String host;
final String user;
final String password;
final bool enabled;
final bool deactivateEcho;
final bool deactivateLiveCheck;
final String serverType; // "gcore" or "geviscope"
ServerModel({
required this.id,
required this.alias,
required this.host,
required this.user,
required this.password,
required this.enabled,
required this.deactivateEcho,
required this.deactivateLiveCheck,
required this.serverType,
});
factory ServerModel.fromJson(Map<String, dynamic> json, String type) {
return ServerModel(
id: json['id'].toString(),
alias: json['alias'] as String,
host: json['host'] as String,
user: json['user'] as String,
password: json['password'] as String,
enabled: json['enabled'] as bool,
deactivateEcho: json['deactivate_echo'] as bool,
deactivateLiveCheck: json['deactivate_live_check'] as bool,
serverType: type,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'alias': alias,
'host': host,
'user': user,
'password': password,
'enabled': enabled,
'deactivate_echo': deactivateEcho,
'deactivate_live_check': deactivateLiveCheck,
};
}
Server toEntity() {
return Server(
id: id,
alias: alias,
host: host,
user: user,
password: password,
enabled: enabled,
deactivateEcho: deactivateEcho,
deactivateLiveCheck: deactivateLiveCheck,
type: serverType == 'gcore' ? ServerType.gcore : ServerType.geviscope,
);
}
factory ServerModel.fromEntity(Server server) {
return ServerModel(
id: server.id,
alias: server.alias,
host: server.host,
user: server.user,
password: server.password,
enabled: server.enabled,
deactivateEcho: server.deactivateEcho,
deactivateLiveCheck: server.deactivateLiveCheck,
serverType: server.type == ServerType.gcore ? 'gcore' : 'geviscope',
);
}
}

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'));
}
}
}

View File

@@ -0,0 +1,77 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/action_template.dart';
import '../../core/constants/api_constants.dart';
/// Service for fetching action templates and categories
/// Used by ActionPickerDialog to show available actions
class ActionTemplateService {
final String baseUrl;
final String? authToken;
ActionTemplateService({
required this.baseUrl,
this.authToken,
});
Map<String, String> get _headers => {
'Content-Type': 'application/json',
if (authToken != null) 'Authorization': 'Bearer $authToken',
};
/// Fetch all action categories
Future<ActionCategoriesResponse> getActionCategories() async {
final url = '$baseUrl/api/v1/configuration/action-categories';
final response = await http.get(
Uri.parse(url),
headers: _headers,
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return ActionCategoriesResponse.fromJson(json);
} else {
throw Exception('Failed to load action categories: ${response.statusCode}');
}
}
/// Fetch all action templates
Future<Map<String, ActionTemplate>> getActionTemplates() async {
final url = '$baseUrl/api/v1/configuration/action-types';
final response = await http.get(
Uri.parse(url),
headers: _headers,
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
final actionTypesMap = json['action_types'] as Map<String, dynamic>;
return actionTypesMap.map((key, value) {
final templateJson = value as Map<String, dynamic>;
templateJson['action_name'] = key; // Add action name to the template
return MapEntry(key, ActionTemplate.fromJson(templateJson));
});
} else {
throw Exception('Failed to load action templates: ${response.statusCode}');
}
}
/// Fetch a specific action template by name
Future<ActionTemplate> getActionTemplate(String actionName) async {
final url = '$baseUrl/api/v1/configuration/action-types/$actionName';
final response = await http.get(
Uri.parse(url),
headers: _headers,
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return ActionTemplate.fromJson(json);
} else if (response.statusCode == 404) {
throw Exception('Action template "$actionName" not found');
} else {
throw Exception('Failed to load action template: ${response.statusCode}');
}
}
}

View File

@@ -0,0 +1,233 @@
import 'package:dartz/dartz.dart';
import '../../core/errors/exceptions.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 '../data_sources/local/action_mapping_local_data_source.dart';
import '../data_sources/remote/action_mapping_remote_data_source.dart';
enum SyncStatus {
idle,
syncing,
success,
error,
}
class SyncResult {
final SyncStatus status;
final String? message;
final int? syncedCount;
final List<String>? errors;
SyncResult({
required this.status,
this.message,
this.syncedCount,
this.errors,
});
factory SyncResult.idle() => SyncResult(status: SyncStatus.idle);
factory SyncResult.syncing() => SyncResult(
status: SyncStatus.syncing,
message: 'Syncing changes...',
);
factory SyncResult.success(int count) => SyncResult(
status: SyncStatus.success,
message: 'Successfully synced $count change${count != 1 ? 's' : ''}',
syncedCount: count,
);
factory SyncResult.error(List<String> errors) => SyncResult(
status: SyncStatus.error,
message: 'Sync failed: ${errors.length} error${errors.length != 1 ? 's' : ''}',
errors: errors,
);
}
abstract class SyncService {
/// Sync all dirty servers with the remote API
Future<Either<Failure, SyncResult>> syncServers();
/// Download fresh data from server and update local storage
Future<Either<Failure, int>> downloadServers();
/// Sync all dirty action mappings with the remote API
Future<Either<Failure, SyncResult>> syncActionMappings();
/// Download fresh action mappings from server and update local storage
Future<Either<Failure, int>> downloadActionMappings();
}
class SyncServiceImpl implements SyncService {
final ServerLocalDataSource localDataSource;
final ServerRemoteDataSource remoteDataSource;
final ActionMappingLocalDataSource actionMappingLocalDataSource;
final ActionMappingRemoteDataSource actionMappingRemoteDataSource;
SyncServiceImpl({
required this.localDataSource,
required this.remoteDataSource,
required this.actionMappingLocalDataSource,
required this.actionMappingRemoteDataSource,
});
@override
Future<Either<Failure, SyncResult>> syncServers() async {
try {
// Get all dirty servers
final dirtyServers = await localDataSource.getDirtyServers();
if (dirtyServers.isEmpty) {
return Right(SyncResult.success(0));
}
final errors = <String>[];
int successCount = 0;
// Process each dirty server
for (final server in dirtyServers) {
try {
final operation = server.syncOperation;
final serverModel = server.toServerModel();
if (operation == 'create') {
await remoteDataSource.createServer(serverModel);
await localDataSource.markServerAsSynced(server.id, server.serverType);
successCount++;
} else if (operation == 'update') {
await remoteDataSource.updateServer(serverModel);
await localDataSource.markServerAsSynced(server.id, server.serverType);
successCount++;
} else if (operation == 'delete') {
await remoteDataSource.deleteServer(server.id, server.serverType);
await localDataSource.markServerAsSynced(server.id, server.serverType);
successCount++;
}
} catch (e) {
errors.add('Failed to sync ${server.alias}: ${e.toString()}');
}
}
if (errors.isEmpty) {
return Right(SyncResult.success(successCount));
} else if (successCount > 0) {
// Partial success
return Right(SyncResult(
status: SyncStatus.success,
message: 'Synced $successCount/${dirtyServers.length} changes',
syncedCount: successCount,
errors: errors,
));
} else {
// Complete failure
return Right(SyncResult.error(errors));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error during sync: ${e.toString()}'));
}
}
@override
Future<Either<Failure, int>> downloadServers() async {
try {
// Fetch all servers from API
final servers = await remoteDataSource.getAllServers();
// Replace local storage (preserving dirty servers)
await localDataSource.replaceAllServers(servers);
return Right(servers.length);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} catch (e) {
return Left(ServerFailure('Unexpected error during download: ${e.toString()}'));
}
}
@override
Future<Either<Failure, SyncResult>> syncActionMappings() async {
try {
// Get all dirty action mappings
final dirtyMappings = await actionMappingLocalDataSource.getDirtyActionMappings();
if (dirtyMappings.isEmpty) {
return Right(SyncResult.success(0));
}
final errors = <String>[];
int successCount = 0;
// Process each dirty action mapping
for (final mapping in dirtyMappings) {
try {
final operation = mapping.syncOperation;
final mappingModel = mapping.toActionMappingModel();
if (operation == 'create') {
await actionMappingRemoteDataSource.createActionMapping(mappingModel);
await actionMappingLocalDataSource.markActionMappingAsSynced(mapping.id);
successCount++;
} else if (operation == 'update') {
await actionMappingRemoteDataSource.updateActionMapping(mapping.id, mappingModel);
await actionMappingLocalDataSource.markActionMappingAsSynced(mapping.id);
successCount++;
} else if (operation == 'delete') {
await actionMappingRemoteDataSource.deleteActionMapping(mapping.id);
await actionMappingLocalDataSource.markActionMappingAsSynced(mapping.id);
successCount++;
}
} catch (e) {
errors.add('Failed to sync ${mapping.name}: ${e.toString()}');
}
}
if (errors.isEmpty) {
return Right(SyncResult.success(successCount));
} else if (successCount > 0) {
// Partial success
return Right(SyncResult(
status: SyncStatus.success,
message: 'Synced $successCount/${dirtyMappings.length} changes',
syncedCount: successCount,
errors: errors,
));
} else {
// Complete failure
return Right(SyncResult.error(errors));
}
} on ActionMappingException catch (e) {
return Left(ActionMappingFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} catch (e) {
return Left(ActionMappingFailure('Unexpected error during sync: ${e.toString()}'));
}
}
@override
Future<Either<Failure, int>> downloadActionMappings() async {
try {
// Fetch all action mappings from API
final mappings = await actionMappingRemoteDataSource.getAllActionMappings();
// Replace local storage with force=true to discard local changes and get fresh server data
await actionMappingLocalDataSource.replaceAllActionMappings(mappings, force: true);
return Right(mappings.length);
} on ActionMappingException catch (e) {
return Left(ActionMappingFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} catch (e) {
return Left(ActionMappingFailure('Unexpected error during download: ${e.toString()}'));
}
}
}