Merge pull request 'Crackhead Programming be like' (#34) from enhance/gameplay_stats_query into master
Reviewed-on: GibCasa/GameServerSupervisor#34
This commit is contained in:
		
						commit
						c32c779313
					
				
							
								
								
									
										18
									
								
								webpanel/migrations/0014_server_query_protocol.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								webpanel/migrations/0014_server_query_protocol.py
									
									
									
									
									
										Normal file
									
								
							@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										23
									
								
								webpanel/migrations/0016_game_default_query_port_key.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								webpanel/migrations/0016_game_default_query_port_key.py
									
									
									
									
									
										Normal file
									
								
							@ -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,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -3,34 +3,195 @@
 | 
			
		||||
{% block title %}{{ game.name }} - Game Details{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <h2>{{ game.name }}</h2>
 | 
			
		||||
<h2>{{ game.name }}</h2>
 | 
			
		||||
 | 
			
		||||
{# --- Add the CSS from previous example for .server-box etc. --- #}
 | 
			
		||||
<style>
 | 
			
		||||
    .server-box {
 | 
			
		||||
        border: 1px solid #555;
 | 
			
		||||
        padding: 15px;
 | 
			
		||||
        margin-bottom: 15px;
 | 
			
		||||
        background-color: #2a2a2a;
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    <div class="game-detail">
 | 
			
		||||
        <div class="game-box" style="width: 150px; text-align: center;">
 | 
			
		||||
            <a href="{% url 'game_detail' game.name %}">
 | 
			
		||||
                <img src="{{ game.thumbnail.url }}" alt="{{ game.name }}" style="width: 100%; height: auto;">
 | 
			
		||||
                <p>{{ game.name }}</p>
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="active-servers">
 | 
			
		||||
            {% if active_servers %}
 | 
			
		||||
            {% for server in active_servers %}
 | 
			
		||||
                <fieldset class="server-box">
 | 
			
		||||
                    <legend>Server: {{ game.name }}</legend>
 | 
			
		||||
                    <div class="server-details">
 | 
			
		||||
                        <p><strong>IP Address:</strong> {{ server.ip_address }}</p>
 | 
			
		||||
                        <p><strong>Port:</strong> {{ server.port }}</p>
 | 
			
		||||
                        <p><strong>Status:</strong> Online</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </fieldset>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
            {% else %}
 | 
			
		||||
                <p>No active servers found for this game.</p>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="game-info">
 | 
			
		||||
            <p>Pull data from some open API to populate information about the game and render it.</p>
 | 
			
		||||
        </div>
 | 
			
		||||
    .server-box legend {
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        color: #0f0;
 | 
			
		||||
        /* Lime green */
 | 
			
		||||
        padding: 0 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .server-details p {
 | 
			
		||||
        margin: 6px 0;
 | 
			
		||||
        font-size: 0.95em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .live-stats {
 | 
			
		||||
        margin-top: 10px;
 | 
			
		||||
        padding-top: 10px;
 | 
			
		||||
        border-top: 1px dashed #444;
 | 
			
		||||
        color: #eee;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .live-stats strong {
 | 
			
		||||
        color: #bbb;
 | 
			
		||||
        width: 120px;
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Align labels */
 | 
			
		||||
    .player-list {
 | 
			
		||||
        list-style: none;
 | 
			
		||||
        padding-left: 15px;
 | 
			
		||||
        font-size: 0.9em;
 | 
			
		||||
        color: #ccc;
 | 
			
		||||
        max-height: 150px;
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
        margin-top: 5px;
 | 
			
		||||
        border-left: 2px solid #444;
 | 
			
		||||
        padding-left: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .player-list li {
 | 
			
		||||
        margin-bottom: 3px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .error-msg {
 | 
			
		||||
        color: #ff8888;
 | 
			
		||||
        font-style: italic;
 | 
			
		||||
        font-size: 0.9em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .game-thumbnail {
 | 
			
		||||
        float: left;
 | 
			
		||||
        margin-right: 20px;
 | 
			
		||||
        margin-bottom: 20px;
 | 
			
		||||
        width: 150px;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .server-lists {
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .clear-float {
 | 
			
		||||
        clear: both;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h3 {
 | 
			
		||||
        margin-top: 25px;
 | 
			
		||||
        color: #ccc;
 | 
			
		||||
        border-bottom: 1px solid #555;
 | 
			
		||||
        padding-bottom: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hr.section-divider {
 | 
			
		||||
        margin: 30px 0;
 | 
			
		||||
        border: 0;
 | 
			
		||||
        border-top: 1px solid #444;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<div class="game-detail">
 | 
			
		||||
    <div class="game-thumbnail">
 | 
			
		||||
        {% if game.thumbnail %}
 | 
			
		||||
        <img src="{{ game.thumbnail.url }}" alt="{{ game.name }}"
 | 
			
		||||
            style="width: 100%; height: auto; border: 1px solid #444;">
 | 
			
		||||
        {% else %}
 | 
			
		||||
        <div
 | 
			
		||||
            style="width: 150px; height: 200px; background-color: #333; display: flex; align-items: center; justify-content: center; color: #888; border: 1px solid #444;">
 | 
			
		||||
            No Thumbnail</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        <p style="margin-top: 5px;">{{ game.name }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
    <div class="server-lists">
 | 
			
		||||
        <h3>Active Servers</h3>
 | 
			
		||||
        {% if active_servers %}
 | 
			
		||||
        {% for server in active_servers %}
 | 
			
		||||
        {# --- CORRECTED FIELDSET FOR ACTIVE SERVERS --- #}
 | 
			
		||||
        <fieldset class="server-box">
 | 
			
		||||
            <legend>{{ server.name }}</legend>
 | 
			
		||||
            <div class="server-details">
 | 
			
		||||
                <p><strong>Podman Status:</strong> <span style="color: limegreen;">Online</span></p>
 | 
			
		||||
                <p><strong>IP Address:</strong> {{ server.ip_address|default:"N/A" }}</p>
 | 
			
		||||
                <p><strong>Port Mappings:</strong> {{ server.get_ports_display }}</p>
 | 
			
		||||
                {# --- Live Stats Section (Checks Game Model Config & Query Result) --- #}
 | 
			
		||||
                <div class="live-stats">
 | 
			
		||||
                    {# Check if query succeeded and returned data #}
 | 
			
		||||
                    {% if server.live_stats %}
 | 
			
		||||
                    <p><strong>Live Status:</strong> <span style="color: lightgreen;">Reachable</span></p>
 | 
			
		||||
                    {# Display common stats using consistent keys from the dictionary #}
 | 
			
		||||
                    <p><strong>Server Name:</strong> {{ server.live_stats.server_name|default:"N/A" }}</p>
 | 
			
		||||
                    <p><strong>Map:</strong> {{ server.live_stats.map_name|default:"N/A" }}</p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                        <strong>Players:</strong>
 | 
			
		||||
                        {{ 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 %}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                        <strong>Password:</strong>
 | 
			
		||||
                        {% if server.live_stats.password_protected %}<span style="color: orange;">Yes</span>{% else
 | 
			
		||||
                        %}No{% endif %}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    {% if server.live_stats.vac_enabled is not None %}
 | 
			
		||||
                    <p><strong>VAC Secured:</strong> {% if server.live_stats.vac_enabled %}Yes{% else %}No{% endif %}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if server.live_stats.players_list %} {# Raw list, e.g. from Q2 #}
 | 
			
		||||
                    <p><strong>Player List (Raw):</strong></p>
 | 
			
		||||
                    <ul class="player-list">
 | 
			
		||||
                        {% for player_line in server.live_stats.players_list %}<li>{{ player_line }}</li>{% empty %}<li>
 | 
			
		||||
                        </li>{% endfor %}
 | 
			
		||||
                    </ul>
 | 
			
		||||
                    {% 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 %}
 | 
			
		||||
                    <p class="error-msg"><strong>Live Status:</strong> Unreachable (Timeout or Error)</p>
 | 
			
		||||
                    {# Query was not configured for this game in the Game model #}
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <p><em>(Live query not configured for {{ server.game.name }})</em></p>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
                {# --- End Live Stats Section --- #}
 | 
			
		||||
            </div>
 | 
			
		||||
        </fieldset>
 | 
			
		||||
        {# --- END CORRECTED FIELDSET --- #}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        {% else %}
 | 
			
		||||
        <p>No active servers found for this game.</p>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        <hr class="section-divider"> {# Use HR for clearer separation #}
 | 
			
		||||
 | 
			
		||||
        <h3>Dormant Servers</h3>
 | 
			
		||||
        {% if dormant_servers %}
 | 
			
		||||
        {% for server in dormant_servers %}
 | 
			
		||||
        <fieldset class="server-box" style="opacity: 0.6;">
 | 
			
		||||
            <legend>{{ server.name }}</legend>
 | 
			
		||||
            <div class="server-details">
 | 
			
		||||
                <p><strong>Podman Status:</strong> <span style="color: orange;">{{ server.status|title }}</span></p>
 | 
			
		||||
                <p><strong>Image:</strong> {{ server.image }}</p>
 | 
			
		||||
                <p><strong>Connect Info:</strong> {{ server.ip_address|default:"N/A" }} : {{ server.get_ports_display }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </fieldset>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        {% else %}
 | 
			
		||||
        <p>No dormant servers found for this game.</p>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
    </div> {# End server-lists #}
 | 
			
		||||
 | 
			
		||||
    <div class="clear-float"></div> {# Clear float #}
 | 
			
		||||
 | 
			
		||||
    <div class="game-info" style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #444;">
 | 
			
		||||
        <h4>About {{ game.name }}</h4>
 | 
			
		||||
        <p>Genre: {{ game.genre|default:"N/A" }}</p>
 | 
			
		||||
        {# TODO: Pull more data #}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
    })
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user