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:
159
geutebruck_app/lib/core/network/dio_client.dart
Normal file
159
geutebruck_app/lib/core/network/dio_client.dart
Normal file
@@ -0,0 +1,159 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../constants/api_constants.dart';
|
||||
import '../storage/token_manager.dart';
|
||||
|
||||
class DioClient {
|
||||
late final Dio _dio;
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
|
||||
DioClient(this._secureStorage) {
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: ApiConstants.basePath,
|
||||
connectTimeout: ApiConstants.connectTimeout,
|
||||
receiveTimeout: ApiConstants.receiveTimeout,
|
||||
sendTimeout: ApiConstants.sendTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Add interceptors
|
||||
_dio.interceptors.add(_authInterceptor());
|
||||
_dio.interceptors.add(_loggingInterceptor());
|
||||
}
|
||||
|
||||
Dio get dio => _dio;
|
||||
|
||||
// Auth interceptor - adds token to requests
|
||||
Interceptor _authInterceptor() {
|
||||
return InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
// Skip token for login endpoint
|
||||
if (options.path.contains(ApiConstants.loginEndpoint)) {
|
||||
return handler.next(options);
|
||||
}
|
||||
|
||||
// Add access token to other requests
|
||||
String? token;
|
||||
try {
|
||||
token = await _secureStorage.read(key: 'access_token');
|
||||
} catch (e) {
|
||||
// Fallback to memory storage on web
|
||||
print('Reading token from memory storage');
|
||||
}
|
||||
|
||||
// Fallback to TokenManager if secure storage fails
|
||||
token ??= TokenManager().accessToken;
|
||||
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
return handler.next(options);
|
||||
},
|
||||
onError: (error, handler) async {
|
||||
// Handle 401 errors (token expired)
|
||||
if (error.response?.statusCode == 401) {
|
||||
// Try to refresh token
|
||||
final refreshed = await _refreshToken();
|
||||
|
||||
if (refreshed) {
|
||||
// Retry the original request
|
||||
try {
|
||||
final response = await _retry(error.requestOptions);
|
||||
return handler.resolve(response);
|
||||
} catch (e) {
|
||||
return handler.next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handler.next(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Logging interceptor - logs requests and responses
|
||||
Interceptor _loggingInterceptor() {
|
||||
return InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
print('🔵 REQUEST[${options.method}] => ${options.uri}');
|
||||
print('Headers: ${options.headers}');
|
||||
if (options.data != null) {
|
||||
print('Body: ${options.data}');
|
||||
}
|
||||
return handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
print('🟢 RESPONSE[${response.statusCode}] => ${response.requestOptions.uri}');
|
||||
print('Data: ${response.data}');
|
||||
return handler.next(response);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
print('🔴 ERROR[${error.response?.statusCode}] => ${error.requestOptions.uri}');
|
||||
print('Message: ${error.message}');
|
||||
if (error.response?.data != null) {
|
||||
print('Error Data: ${error.response?.data}');
|
||||
}
|
||||
return handler.next(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Refresh token logic
|
||||
Future<bool> _refreshToken() async {
|
||||
try {
|
||||
String? refreshToken;
|
||||
try {
|
||||
refreshToken = await _secureStorage.read(key: 'refresh_token');
|
||||
} catch (e) {
|
||||
// Fallback to memory storage on web
|
||||
refreshToken = TokenManager().refreshToken;
|
||||
}
|
||||
|
||||
refreshToken ??= TokenManager().refreshToken;
|
||||
if (refreshToken == null) return false;
|
||||
|
||||
final response = await _dio.post(
|
||||
ApiConstants.refreshEndpoint,
|
||||
data: {'refresh_token': refreshToken},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final newAccessToken = response.data['access_token'];
|
||||
// Save to memory first
|
||||
TokenManager().saveTokens(accessToken: newAccessToken);
|
||||
try {
|
||||
await _secureStorage.write(key: 'access_token', value: newAccessToken);
|
||||
} catch (e) {
|
||||
// Ignore secure storage errors on web
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
print('Failed to refresh token: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Retry a request
|
||||
Future<Response> _retry(RequestOptions requestOptions) async {
|
||||
final options = Options(
|
||||
method: requestOptions.method,
|
||||
headers: requestOptions.headers,
|
||||
);
|
||||
|
||||
return _dio.request(
|
||||
requestOptions.path,
|
||||
data: requestOptions.data,
|
||||
queryParameters: requestOptions.queryParameters,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user