Files
geutebruck/flutter_action_mapping_redesign.md
Administrator 14893e62a5 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>
2025-12-31 18:10:54 +01:00

674 lines
22 KiB
Markdown

# Flutter Action Mapping Redesign - Based on Native GeViSet App
## Analysis of Native GeViSet UI
### Key UI Patterns Observed
#### 1. **Main List View** (Action_mapping_list.png)
- **Table/DataGrid layout** with columns:
- Input action (name/description)
- Actions (count)
- Output action 1, 2, 3, 4... (shows action names)
- **Selected row highlighting** (blue background)
- **Bottom action bar**: Add, Edit, Remove buttons
- **Horizontal scrolling** for multiple output actions
#### 2. **Mapping Settings Dialog** (Action_mapping_settings.png)
- **Two sections**:
- Input action (single field with browse button)
- Output actions (list with management controls)
- **List management controls**: + (add), ⚙ (edit), - (remove), ▲ ▼ (reorder)
- **Modal dialog** approach (not inline editing)
#### 3. **Action Settings Dialog** (Input_action.png, Output_CrossSwitch.png, Output_PanStop.png)
- **Three-pane layout**:
1. **Left pane**: Category dropdown + scrollable action list
2. **Right pane**: Parameters section (dynamic based on action type)
3. **Bottom**: Caption field, Delay execution, Description text
- **Category-based organization** (Telemetry control, Crossbar control, GSC: Camera control, etc.)
- **Action descriptions** shown at bottom (helps users understand what each action does)
- **Dynamic parameter widgets**:
- Checkboxes with spinners
- Text fields with browse buttons
- Table format (Parameter | Value)
- **Default button** to reset to defaults
---
## Proposed Flutter App Redesign
### Architecture Changes
#### 1. **Add Action Categories to Backend**
Update `ACTION_PARAMETER_TEMPLATES` to include categories:
```python
# In configuration.py
ACTION_CATEGORIES = {
"Camera Control": ["PanLeft", "PanRight", "PanStop", "TiltUp", ...],
"Video Switching": ["CrossSwitch OpCon -> Matrix", "CrossSwitch C -> M"],
"Recording": ["StartRecording", "StopRecording"],
"Events": ["StartEvent", "StopEvent", "KillEvent"],
"Alarms": ["AlarmRecording"],
"Digital I/O": ["DigitalContactActivate", "DigitalContactDeactivate"],
"Viewer": ["GSC ViewerConnectLive V <- C", "GSC ViewerDisconnect"]
}
```
Add endpoint:
```
GET /api/v1/configuration/action-categories
```
#### 2. **Add Action Descriptions**
Enhance `ACTION_PARAMETER_TEMPLATES` with descriptions:
```python
"PanStop": {
"parameters": ["GCoreServer", "PTZ head"],
"description": "Stop pan movement. The panning of the camera will be stopped.",
"category": "Camera Control",
"required_caption": True,
"supports_delay": True
}
```
---
### UI Component Structure
```
ActionMappingsScreen
├── ActionMappingsList (DataTable/ListView)
│ ├── Column: Input Action
│ ├── Column: # Actions
│ ├── Column: Output Action 1
│ ├── Column: Output Action 2
│ ├── Column: Output Action 3
│ └── BottomBar: [Add] [Edit] [Remove]
└── ActionMappingDialog (when Add/Edit clicked)
├── InputActionField
│ └── [Browse] button → ActionPickerDialog
├── OutputActionsSection
│ ├── OutputActionsList
│ └── Controls: [+] [⚙] [-] [▲] [▼]
└── [OK] [Cancel]
ActionPickerDialog (for selecting/editing an action)
├── CategoryDropdown
├── ActionsList (filtered by category)
├── ParametersPanel (dynamic based on selected action)
│ └── DynamicParameterFields
├── CaptionField (required)
├── DelayExecutionField
├── DescriptionText (read-only, shows action description)
└── [Default] [OK] [Cancel]
```
---
### Screen-by-Screen Design
#### Screen 1: Action Mappings List (Main Screen)
**Layout**: Desktop-style DataTable (use DataTable widget or custom ListView)
```dart
class ActionMappingsListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Action Mappings')),
body: Column(
children: [
// DataTable with horizontal scrolling
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: [
DataColumn(label: Text('Input Action')),
DataColumn(label: Text('Actions')),
DataColumn(label: Text('Output Action 1')),
DataColumn(label: Text('Output Action 2')),
DataColumn(label: Text('Output Action 3')),
DataColumn(label: Text('Output Action 4')),
],
rows: _buildRows(),
),
),
),
// Bottom action bar
Container(
padding: EdgeInsets.all(8),
child: Row(
children: [
ElevatedButton(
onPressed: _onAddMapping,
child: Text('Add...'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: _onEditMapping,
child: Text('Edit...'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: _onRemoveMapping,
child: Text('Remove'),
),
Spacer(),
ElevatedButton(
onPressed: _onOk,
child: Text('OK'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: _onCancel,
child: Text('Cancel'),
),
],
),
),
],
),
);
}
}
```
**Key Features**:
- Row selection (highlight selected row in blue)
- Show input action name
- Show count of output actions
- Show first 4 output action names in separate columns
- Horizontal scrolling if more columns needed
---
#### Screen 2: Action Mapping Settings Dialog
**Layout**: Dialog with input section and output actions list
```dart
class ActionMappingDialog extends StatefulWidget {
final ActionMapping? mapping; // null for new, existing for edit
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 600,
height: 400,
child: Column(
children: [
// Title
DialogTitle(text: 'Action mapping settings'),
// Input action section
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Input action', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _inputActionController,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: _onBrowseInputAction,
child: Text('...'),
),
],
),
],
),
),
// Output actions section
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Output actions', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
],
),
),
// Output actions list
Expanded(
child: Container(
margin: EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
),
child: ListView.builder(
itemCount: outputActions.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(outputActions[index].action),
selected: selectedOutputIndex == index,
onTap: () => setState(() => selectedOutputIndex = index),
);
},
),
),
),
// Output actions controls
Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(icon: Icon(Icons.add), onPressed: _onAddOutput),
IconButton(icon: Icon(Icons.settings), onPressed: _onEditOutput),
IconButton(icon: Icon(Icons.remove), onPressed: _onRemoveOutput),
IconButton(icon: Icon(Icons.arrow_upward), onPressed: _onMoveUp),
IconButton(icon: Icon(Icons.arrow_downward), onPressed: _onMoveDown),
],
),
),
// Bottom buttons
Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(onPressed: _onOk, child: Text('OK')),
SizedBox(width: 8),
ElevatedButton(onPressed: _onCancel, child: Text('Cancel')),
],
),
),
],
),
),
);
}
}
```
---
#### Screen 3: Action Picker Dialog (most important!)
**Layout**: Three-section layout matching native app
```dart
class ActionPickerDialog extends StatefulWidget {
final Action? existingAction; // null for new, existing for edit
@override
_ActionPickerDialogState createState() => _ActionPickerDialogState();
}
class _ActionPickerDialogState extends State<ActionPickerDialog> {
String? selectedCategory;
String? selectedAction;
Map<String, ActionTemplate> actionTemplates = {};
Map<String, dynamic> parameters = {};
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 700,
height: 500,
child: Column(
children: [
DialogTitle(text: 'Action settings...'),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// LEFT PANE: Category and Action List
Container(
width: 250,
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Category dropdown
Text('Category:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
DropdownButton<String>(
isExpanded: true,
value: selectedCategory,
items: categories.map((cat) =>
DropdownMenuItem(value: cat, child: Text(cat))
).toList(),
onChanged: (value) {
setState(() {
selectedCategory = value;
selectedAction = null; // Reset action selection
});
},
),
SizedBox(height: 16),
// Action list
Text('Action:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
),
child: ListView.builder(
itemCount: _getFilteredActions().length,
itemBuilder: (context, index) {
final actionName = _getFilteredActions()[index];
return ListTile(
title: Text(actionName),
selected: selectedAction == actionName,
selectedTileColor: Colors.blue,
onTap: () {
setState(() {
selectedAction = actionName;
_loadParametersForAction(actionName);
});
},
);
},
),
),
),
],
),
),
// DIVIDER
VerticalDivider(width: 1),
// RIGHT PANE: Parameters
Expanded(
child: Container(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Parameters section
Text('Parameters:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
// Dynamic parameter fields
Expanded(
child: SingleChildScrollView(
child: _buildParameterFields(),
),
),
Divider(),
// Caption field (required)
Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: 'Caption (required)',
border: OutlineInputBorder(),
),
),
),
SizedBox(width: 16),
Text('Delay execution:'),
SizedBox(width: 8),
SizedBox(
width: 80,
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
suffixText: 'ms',
),
keyboardType: TextInputType.number,
),
),
],
),
SizedBox(height: 8),
// Description (read-only)
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
color: Colors.grey[100],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Description:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)),
SizedBox(height: 4),
Text(
_getActionDescription(),
style: TextStyle(fontSize: 12),
),
],
),
),
],
),
),
),
],
),
),
// Bottom buttons
Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: _onDefault,
child: Text('Default'),
),
Row(
children: [
ElevatedButton(onPressed: _onOk, child: Text('Ok')),
SizedBox(width: 8),
ElevatedButton(onPressed: _onCancel, child: Text('Cancel')),
],
),
],
),
),
],
),
),
);
}
Widget _buildParameterFields() {
if (selectedAction == null) {
return Center(child: Text('Select an action to view parameters'));
}
final template = actionTemplates[selectedAction!];
if (template == null || template.parameters.isEmpty) {
return Center(child: Text('No parameters required'));
}
// Build dynamic parameter fields based on template
return Column(
children: template.parameters.map((param) {
return Padding(
padding: EdgeInsets.only(bottom: 12),
child: Row(
children: [
Checkbox(
value: parameters.containsKey(param),
onChanged: (value) {
setState(() {
if (value == true) {
parameters[param] = '';
} else {
parameters.remove(param);
}
});
},
),
SizedBox(width: 8),
Expanded(
child: TextField(
enabled: parameters.containsKey(param),
decoration: InputDecoration(
labelText: param,
border: OutlineInputBorder(),
),
onChanged: (value) {
parameters[param] = value;
},
),
),
if (_parameterHasBrowseButton(param))
Padding(
padding: EdgeInsets.only(left: 8),
child: ElevatedButton(
onPressed: () => _onBrowseParameter(param),
child: Text('...'),
),
),
],
),
);
}).toList(),
);
}
}
```
---
### Backend API Enhancements Needed
#### 1. Add Action Categories Endpoint
```python
@router.get(
"/action-categories",
status_code=status.HTTP_200_OK,
summary="Get action categories",
description="Get all action types grouped by category"
)
async def get_action_categories(
current_user: User = Depends(require_viewer)
):
"""Get action types organized by category"""
categories = {}
for action_name, template in ACTION_PARAMETER_TEMPLATES.items():
category = template.get("category", "Other")
if category not in categories:
categories[category] = []
categories[category].append(action_name)
return {
"categories": categories,
"total_categories": len(categories)
}
```
#### 2. Enhance Action Templates with More Details
```python
ACTION_PARAMETER_TEMPLATES = {
"PanStop": {
"parameters": ["GCoreServer", "PTZ head"],
"description": "Stop pan movement. The panning of the camera will be stopped.",
"category": "Camera Control",
"required_caption": True,
"supports_delay": True,
"parameter_types": {
"GCoreServer": "dropdown", # Could browse from servers
"PTZ head": "text"
}
},
# ... etc
}
```
---
### Key Differences from Current Flutter App
| Aspect | Current Design | Proposed Design (Like Native) |
|--------|---------------|-------------------------------|
| **Main View** | Card-based list | DataTable with columns |
| **Editing** | Inline expansion | Modal dialogs |
| **Action Selection** | Dropdown only | Category + Scrollable list |
| **Parameters** | Form fields only | Checkboxes + fields (optional parameters) |
| **Descriptions** | Not shown | Shown for each action at bottom |
| **Multiple Outputs** | Not clearly visible | Shown as separate columns in table |
| **Reordering** | Not supported | Up/Down arrow buttons |
| **Visual Style** | Material Design cards | Desktop-style dialogs and tables |
---
### Implementation Priority
**Phase 1: Core Structure**
1. ✅ Backend: Add action categories endpoint
2. ✅ Backend: Enhance templates with descriptions
3. Create ActionPickerDialog with category/action list
4. Implement dynamic parameter fields
**Phase 2: List View**
1. Replace card list with DataTable
2. Add column for output action names
3. Implement row selection
**Phase 3: Dialogs**
1. Create ActionMappingDialog for editing
2. Add output actions list management
3. Implement reordering (up/down)
**Phase 4: Polish**
1. Add descriptions to action picker
2. Add Default button functionality
3. Add delay execution field
4. Improve parameter field types (browse buttons, etc.)
---
### Benefits of This Redesign
1. **Familiar UX for GeViSet users** - Matches their mental model
2. **Better information density** - See more mappings at once
3. **Clearer action relationships** - Input → multiple outputs visible
4. **Category organization** - Easier to find actions
5. **Action descriptions** - Users understand what each action does
6. **Optional parameters** - Checkboxes show what's configurable
7. **Professional desktop feel** - More appropriate for power users
---
### Next Steps
Would you like me to:
1. **Update the backend** to add categories endpoint and enhance templates?
2. **Create a prototype** of the ActionPickerDialog in Flutter?
3. **Design the DataTable** layout for the main list?
4. **All of the above** - full implementation?