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>
22 KiB
22 KiB
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:
- Left pane: Category dropdown + scrollable action list
- Right pane: Parameters section (dynamic based on action type)
- 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:
# 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:
"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)
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
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
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
@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
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
- ✅ Backend: Add action categories endpoint
- ✅ Backend: Enhance templates with descriptions
- Create ActionPickerDialog with category/action list
- Implement dynamic parameter fields
Phase 2: List View
- Replace card list with DataTable
- Add column for output action names
- Implement row selection
Phase 3: Dialogs
- Create ActionMappingDialog for editing
- Add output actions list management
- Implement reordering (up/down)
Phase 4: Polish
- Add descriptions to action picker
- Add Default button functionality
- Add delay execution field
- Improve parameter field types (browse buttons, etc.)
Benefits of This Redesign
- Familiar UX for GeViSet users - Matches their mental model
- Better information density - See more mappings at once
- Clearer action relationships - Input → multiple outputs visible
- Category organization - Easier to find actions
- Action descriptions - Users understand what each action does
- Optional parameters - Checkboxes show what's configurable
- Professional desktop feel - More appropriate for power users
Next Steps
Would you like me to:
- Update the backend to add categories endpoint and enhance templates?
- Create a prototype of the ActionPickerDialog in Flutter?
- Design the DataTable layout for the main list?
- All of the above - full implementation?