Compare commits
1 Commits
master
...
enhance/ga
Author | SHA1 | Date | |
---|---|---|---|
e835462471 |
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