diff --git a/webpanel/migrations/0014_server_query_protocol.py b/webpanel/migrations/0014_server_query_protocol.py new file mode 100644 index 0000000..41e1f48 --- /dev/null +++ b/webpanel/migrations/0014_server_query_protocol.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.5 on 2025-04-23 09:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("webpanel", "0013_migrate_port_to_dict"), + ] + + operations = [ + migrations.AddField( + model_name="server", + name="query_protocol", + field=models.CharField(max_length=20, null=True), + ), + ] diff --git a/webpanel/migrations/0015_remove_server_query_protocol_game_query_protocol.py b/webpanel/migrations/0015_remove_server_query_protocol_game_query_protocol.py new file mode 100644 index 0000000..c50456a --- /dev/null +++ b/webpanel/migrations/0015_remove_server_query_protocol_game_query_protocol.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.5 on 2025-04-23 10:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("webpanel", "0014_server_query_protocol"), + ] + + operations = [ + migrations.RemoveField( + model_name="server", + name="query_protocol", + ), + migrations.AddField( + model_name="game", + name="query_protocol", + field=models.CharField(max_length=20, null=True), + ), + ] diff --git a/webpanel/migrations/0016_game_default_query_port_key.py b/webpanel/migrations/0016_game_default_query_port_key.py new file mode 100644 index 0000000..d126881 --- /dev/null +++ b/webpanel/migrations/0016_game_default_query_port_key.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.5 on 2025-04-23 10:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("webpanel", "0015_remove_server_query_protocol_game_query_protocol"), + ] + + operations = [ + migrations.AddField( + model_name="game", + name="default_query_port_key", + field=models.CharField( + blank=True, + help_text="Default key (e.g., '27015/udp' or '27910') used for status queries within the Server's port mappings.", + max_length=20, + null=True, + ), + ), + ] diff --git a/webpanel/models.py b/webpanel/models.py index 675192a..abc9221 100644 --- a/webpanel/models.py +++ b/webpanel/models.py @@ -8,6 +8,13 @@ class Game(models.Model): name = models.CharField(max_length=100) genre = models.CharField(max_length=50, blank=True, null=True) thumbnail = models.ImageField(upload_to='game_thumbnails/', null=True, blank=True) + query_protocol=models.CharField(max_length=20, null=True) + default_query_port_key = models.CharField( + max_length=20, + blank=True, + null=True, + help_text="Default key (e.g., '27015/udp' or '27910') used for status queries within the Server's port mappings." + ) def __str__(self) -> str: return self.name diff --git a/webpanel/templates/webpanel/game_detail.html b/webpanel/templates/webpanel/game_detail.html index 7151114..31c3064 100644 --- a/webpanel/templates/webpanel/game_detail.html +++ b/webpanel/templates/webpanel/game_detail.html @@ -3,34 +3,195 @@ {% block title %}{{ game.name }} - Game Details{% endblock %} {% block content %} -

{{ game.name }}

+

{{ game.name }}

+{# --- Add the CSS from previous example for .server-box etc. --- #} + + +
+
+ {% if game.thumbnail %} + {{ game.name }} + {% else %} +
+ No Thumbnail
+ {% endif %} +

{{ game.name }}

-{% endblock %} + +
+

Active Servers

+ {% if active_servers %} + {% for server in active_servers %} + {# --- CORRECTED FIELDSET FOR ACTIVE SERVERS --- #} +
+ {{ server.name }} +
+

Podman Status: Online

+

IP Address: {{ server.ip_address|default:"N/A" }}

+

Port Mappings: {{ server.get_ports_display }}

+ {# --- Live Stats Section (Checks Game Model Config & Query Result) --- #} +
+ {# Check if query succeeded and returned data #} + {% if server.live_stats %} +

Live Status: Reachable

+ {# Display common stats using consistent keys from the dictionary #} +

Server Name: {{ server.live_stats.server_name|default:"N/A" }}

+

Map: {{ server.live_stats.map_name|default:"N/A" }}

+

+ Players: + {{ server.live_stats.current_players|default:"0" }} / + {{ server.live_stats.max_players|default:"?" }} + {% if server.live_stats.bots is not None %} ({{ server.live_stats.bots }} bots){% endif %} +

+

+ Password: + {% if server.live_stats.password_protected %}Yes{% else + %}No{% endif %} +

+ {% if server.live_stats.vac_enabled is not None %} +

VAC Secured: {% if server.live_stats.vac_enabled %}Yes{% else %}No{% endif %} +

+ {% endif %} + {% if server.live_stats.players_list %} {# Raw list, e.g. from Q2 #} +

Player List (Raw):

+
    + {% for player_line in server.live_stats.players_list %}
  • {{ player_line }}
  • {% empty %}
  • +
  • {% endfor %} +
+ {% endif %} + + {# Check if query was configured for this game (in Game model) but failed #} + {% elif server.game.query_protocol != 'none' and server.game.default_query_port_key %} +

Live Status: Unreachable (Timeout or Error)

+ {# Query was not configured for this game in the Game model #} + {% else %} +

(Live query not configured for {{ server.game.name }})

+ {% endif %} +
+ {# --- End Live Stats Section --- #} +
+
+ {# --- END CORRECTED FIELDSET --- #} + {% endfor %} + {% else %} +

No active servers found for this game.

+ {% endif %} + +
{# Use HR for clearer separation #} + +

Dormant Servers

+ {% if dormant_servers %} + {% for server in dormant_servers %} +
+ {{ server.name }} +
+

Podman Status: {{ server.status|title }}

+

Image: {{ server.image }}

+

Connect Info: {{ server.ip_address|default:"N/A" }} : {{ server.get_ports_display }} +

+
+
+ {% endfor %} + {% else %} +

No dormant servers found for this game.

+ {% endif %} + +
{# End server-lists #} + +
{# Clear float #} + +
+

About {{ game.name }}

+

Genre: {{ game.genre|default:"N/A" }}

+ {# TODO: Pull more data #} +
+
+{% endblock %} \ No newline at end of file diff --git a/webpanel/utils.py b/webpanel/utils.py index e69de29..511ac90 100644 --- a/webpanel/utils.py +++ b/webpanel/utils.py @@ -0,0 +1,12 @@ +import a2s + +# games tested by library author +# Half-Life 2, Half-Life, Team Fortress 2, Counter-Strike: Global Offensive, +# Counter-Strike 1.6, ARK: Survival Evolved, Rust + + +def a2s_query(ip, port): + address = (ip, port) + info = a2s.info(address) + return info + diff --git a/webpanel/views.py b/webpanel/views.py index 0035e2f..4184ebf 100644 --- a/webpanel/views.py +++ b/webpanel/views.py @@ -1,5 +1,15 @@ from django.shortcuts import render, get_object_or_404 from .models import Game, Server +from .utils import a2s_query +import logging + +logger = logging.getLogger(__name__) + +PROTOCOL_FUNCTION_MAP = { + 'a2s': a2s_query, + # Add other protocols here -> 'quake2': query_quake2, etc. +} + def home(request): """Display the home page with links to other views.""" @@ -11,12 +21,75 @@ def games(request): return render(request, 'webpanel/games.html', {'games': games}) def game_detail(request, game_name): - print(f"Looking for game: {game_name}") - game = get_object_or_404(Game, name=game_name) - servers = Server.objects.filter(game=game) - for server in servers: - server.sync_status() - dormant_servers = servers.filter(status='offline') - active_servers = servers.filter(status='online') - return render(request, 'webpanel/game_detail.html', {'game': game, - 'active_servers': active_servers, 'dormant_servers': dormant_servers}) + logger.info(f"Accessing game detail for: {game_name}") # Use logger.info or logger.debug + game = get_object_or_404(Game, name__iexact=game_name) # Use iexact for case-insensitivity + + query_protocol = getattr(game, 'query_protocol', 'none') # Safely get protocol + query_function = PROTOCOL_FUNCTION_MAP.get(query_protocol) + default_port_key = getattr(game, 'default_query_port_key', None) # Safely get port key + + if not query_function: + logger.debug(f"Live query disabled for game '{game.name}' (protocol '{query_protocol}' not mapped or 'none').") + elif not default_port_key: + logger.warning(f"Live query configured for '{game.name}' (protocol '{query_protocol}') but 'Default query port key' is not set in Game model.") + else: + logger.debug(f"Live query configured for '{game.name}': Protocol='{query_protocol}', Port Key='{default_port_key}'") + servers_queryset = Server.objects.filter(game=game) + + active_server_list = [] + dormant_server_list = [] + + for server in servers_queryset: + server.sync_status() + server.live_stats = None + + if server.status == 'online' and a2s_query and default_port_key: + logger.debug(f"Server '{server.name}' is online, attempting query using protocol '{query_protocol}'.") + + query_port_host = None + port_keys_to_try = [default_port_key] + if '/' in default_port_key: + port_keys_to_try.append(default_port_key.split('/')[0]) # Try without /udp suffix + + for key in port_keys_to_try: + if isinstance(server.port, dict) and key in server.port: # Check if server.port is a dict + try: + query_port_host = int(server.port[key]) + logger.debug(f"Found host query port {query_port_host} using key '{key}'.") + break + except (ValueError, TypeError): + logger.warning(f"Invalid port value '{server.port[key]}' for key '{key}' in server '{server.name}'.") + query_port_host = None + + if query_port_host is None: + logger.warning(f"Could not find host port for key '{default_port_key}' (or fallback) in server '{server.name}'. Port data: {server.port}") + + ip_to_query = server.ip_address + if not ip_to_query or ip_to_query in ["0.0.0.0", "::"]: + ip_to_query = "127.0.0.1" # Default to localhost + logger.debug(f"Using IP {ip_to_query} for query.") + + if ip_to_query and query_port_host is not None: + try: + logger.info(f"Querying {game.name} server '{server.name}' at {ip_to_query}:{query_port_host}") + server.live_stats = a2s_query(ip=ip_to_query, port=query_port_host) # Call the mapped function + if server.live_stats: + logger.info(f"Query successful for '{server.name}'.") # Add more detail if needed + else: + logger.warning(f"Query returned no data for '{server.name}'.") + except Exception as e: + logger.error(f"Error querying server '{server.name}': {e}", exc_info=True) + server.live_stats = None # Ensure it's None on error + else: + logger.warning(f"Skipping query for '{server.name}': Missing IP or Host Port.") + + if server.status == 'online': + active_server_list.append(server) + else: + dormant_server_list.append(server) + + return render(request, 'webpanel/game_detail.html', { + 'game': game, + 'active_servers': active_server_list, # Use the list we built + 'dormant_servers': dormant_server_list # Use the list we built + }) \ No newline at end of file