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

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:
    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:

# 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

  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?