Files
chrani-bot-tng/DOM_HELPER_MIGRATION.md
2025-11-21 07:26:02 +01:00

9.5 KiB

DOM Helper Migration Plan

Step 1: Add Helpers to Module Base Class

File: bot/module.py

Add two methods to Module class:

def get_element_from_dom(self, module_name, dataset, owner, identifier=None):
    """
    Get full element from DOM.

    Args:
        module_name: e.g. "module_locations"
        dataset: active dataset name
        owner: owner steamid
        identifier: element identifier (optional for owner-level access)

    Returns:
        Full element dict from DOM
    """
    path_data = (
        self.dom.data
        .get(module_name, {})
        .get("elements", {})
        .get(dataset, {})
        .get(owner, {})
    )

    if identifier:
        return path_data.get(identifier, {})

    return path_data


def update_element(self, module_name, dataset, owner, identifier, updates, dispatchers_steamid=None):
    """
    Update element fields in DOM.

    Args:
        module_name: e.g. "module_locations"
        dataset: active dataset name
        owner: owner steamid
        identifier: element identifier
        updates: dict with fields to update
        dispatchers_steamid: who triggered update

    Example:
        module.update_element("module_locations", dataset, owner, "Lobby",
                            {"is_enabled": True})
    """
    # Get current element
    element = self.get_element_from_dom(module_name, dataset, owner, identifier)

    # Merge updates
    element.update(updates)

    # Write back full element
    self.dom.data.upsert({
        module_name: {
            "elements": {
                dataset: {
                    owner: {
                        identifier: element
                    }
                }
            }
        }
    }, dispatchers_steamid=dispatchers_steamid, min_callback_level=4, max_callback_level=5)


def get_element_from_callback(self, updated_values_dict, matched_path):
    """
    Get full element from DOM based on callback data.

    Args:
        updated_values_dict: Dict from callback kwargs
        matched_path: Pattern with wildcards like "module_x/elements/%dataset%/%owner%/%id%"

    Returns:
        Full element dict from DOM

    Notes:
        - matched_path contains wildcards (e.g., %owner_steamid%)
        - Wildcards are placeholders, not actual values
        - Actual values come from updated_values_dict structure
        - Works with any depth (4, 5, 6+)
    """
    parts = matched_path.split('/')

    if 'elements' not in parts or len(parts) < 4:
        return {}

    active_dataset = self.dom.data.get("module_game_environment", {}).get("active_dataset")
    module_name = parts[0]

    # Depth 5+: module/elements/dataset/owner/identifier[/property]
    # Structure: {identifier: {data}}
    if len(parts) >= 5:
        identifier = list(updated_values_dict.keys())[0]
        element_dict = updated_values_dict[identifier]
        owner = element_dict.get("owner")

        # Get full element (ignoring property path if depth > 5)
        return self.get_element_from_dom(module_name, active_dataset, owner, identifier)

    # Depth 4: module/elements/dataset/owner
    # Structure: {owner: {...}}
    elif len(parts) == 4:
        owner = list(updated_values_dict.keys())[0]
        return self.get_element_from_dom(module_name, active_dataset, owner)

    return {}

Step 2: Update All Actions

Find all actions:

grep -r "module.dom.data.upsert" bot/modules/*/actions/*.py

Pattern - OLD:

module.dom.data.upsert({
    "module_locations": {
        "elements": {
            location_origin: {
                location_owner: {
                    location_identifier: {
                        "is_enabled": element_is_enabled
                    }
                }
            }
        }
    }
}, dispatchers_steamid=dispatchers_steamid, min_callback_level=4)

Pattern - NEW:

active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset")

module.update_element(
    "module_locations",
    active_dataset,
    location_owner,
    location_identifier,
    {"is_enabled": element_is_enabled},
    dispatchers_steamid=dispatchers_steamid
)

Actions checklist:

  • locations/actions/toggle_enabled_flag.py
  • locations/actions/edit_location.py (keep as-is, already writes full element)
  • players/actions/update_player_permission_level.py
  • players/actions/toggle_player_mute.py
  • dom_management/actions/select.py
  • All other actions with partial upserts

Step 3: Update All Handlers

Find all handlers:

python3 scripts/analyze_callback_usage.py

Pattern - OLD:

def handler(*args, **kwargs):
    module = args[0]
    updated_values_dict = kwargs.get("updated_values_dict", {})

    for identifier, data in updated_values_dict.items():
        owner = data.get("owner")
        # Complex DOM lookups...
        full_element = module.dom.data.get(...)...

Pattern - NEW:

def handler(*args, **kwargs):
    module = args[0]
    updated_values_dict = kwargs.get("updated_values_dict", {})
    matched_path = kwargs.get("matched_path", "")

    for identifier, data in updated_values_dict.items():
        # Get full element
        element = module.get_element_from_callback(
            {identifier: data},
            matched_path
        )

        # Use directly
        owner = element.get("owner")
        name = element.get("name")
        # ...

Handlers checklist:

  • locations/widgets/manage_locations_widget.py::table_row
  • locations/widgets/manage_locations_widget.py::update_location_on_map
  • locations/widgets/manage_locations_widget.py::update_selection_status
  • locations/widgets/manage_locations_widget.py::update_enabled_flag
  • players/widgets/manage_players_widget.py::table_rows
  • players/widgets/manage_players_widget.py::update_widget
  • game_environment/widgets/location_change_announcer_widget.py::announce_location_change

Step 4: Testing

For each file:

  1. Update code
  2. Restart bot
  3. Trigger action/handler
  4. Check logs for errors
  5. Verify UI updates

Automated check:

# No more complex upserts:
grep -r "\"elements\":" bot/modules/*/actions/*.py | wc -l
# Should be 0 or very few (only edit_location type actions)

# No more DOM fallback in handlers:
grep -r "\.get.*\.get.*\.get.*\.get" bot/modules/*/widgets/*.py | wc -l
# Should be much lower

Step 5: Validate Wildcards and Browser Updates

Critical check - wildcards MUST work:

Handler registration uses wildcards:

"module_locations/elements/%map_identifier%/%owner_steamid%/%element_identifier%": handler

Callback system passes matched_path with wildcards intact. Helper get_element_from_callback() MUST parse this correctly.

Test each wildcard pattern:

# Test location handler (depth 5):
# Pattern: module_locations/elements/%map%/%owner%/%id%
# Edit location → check handler receives correct element

# Test player handler (depth 4):
# Pattern: module_players/elements/%map%/%steamid%
# Update player → check handler receives correct element

# Test enabled flag (depth 6):
# Pattern: module_locations/elements/%map%/%owner%/%id%/is_enabled
# Toggle enabled → check handler receives correct element

Verify browser updates:

For EACH handler that uses send_data_to_client_hook():

  1. Check handler uses get_element_from_callback() to get full element
  2. Check payload sent to browser is complete
  3. Verify browser JavaScript receives data.payload.{element} with all fields
  4. Test in browser: trigger action → verify immediate update without page refresh

Handlers sending to browser:

  • update_location_on_map → sends location_update
  • table_row → sends table_row
  • update_enabled_flag → sends element_content
  • table_rows (players) → sends table_row
  • update_widget (players) → sends table_row_content

All must send complete elements.

Step 6: Example for Future

New trigger - warn player on low health:

def low_health_warning(*args, **kwargs):
    module = args[0]
    updated_values_dict = kwargs.get("updated_values_dict", {})
    matched_path = kwargs.get("matched_path", "")

    # Get full player element
    for steamid, data in updated_values_dict.items():
        player = module.get_element_from_callback(
            {steamid: data},
            matched_path
        )

        # Check health
        if player.get("health", 100) < 60:
            module.trigger_action_hook(module, event_data=[
                'say_to_player',
                {'steamid': steamid, 'message': 'Low health!'}
            ])

# Register at health property level (depth 5):
widget_meta = {
    "handlers": {
        "module_players/elements/%map_identifier%/%steamid%/health": low_health_warning
    }
}

# Handler receives full player element, not just health value!
# player = {
#     "steamid": "12345",
#     "name": "Player",
#     "health": 45,  ← changed field
#     "pos": {...},
#     ...  ← all other fields
# }

New action - set player flag:

def main_function(module, event_data, dispatchers_steamid):
    steamid = event_data[1].get("steamid")
    flag_value = event_data[1].get("flag_value")

    active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset")

    # Update element
    module.update_element(
        "module_players",
        active_dataset,
        steamid,
        None,  # player has no sub-identifier
        {"custom_flag": flag_value},
        dispatchers_steamid=dispatchers_steamid
    )

Done. Clear pattern for everything.