Better page for clients

parent 005f21b5
...@@ -1419,11 +1419,15 @@ def download_zip(match_id): ...@@ -1419,11 +1419,15 @@ def download_zip(match_id):
@login_required @login_required
@require_active_user @require_active_user
def clients(): def clients():
"""Clients page showing connected clients""" """Clients page showing connected clients with filtering and search"""
try: try:
from app.models import ClientActivity, SystemSettings, APIToken from app.models import ClientActivity, SystemSettings, APIToken
from datetime import datetime, timedelta from datetime import datetime, timedelta
# Get filter and search parameters
status_filter = request.args.get('status') # 'online', 'offline', or None for all
search_query = request.args.get('search', '').strip()
# Get remote domain setting # Get remote domain setting
remote_domain = SystemSettings.get_setting('remote_domain', 'townshipscombatleague.com') remote_domain = SystemSettings.get_setting('remote_domain', 'townshipscombatleague.com')
...@@ -1478,10 +1482,30 @@ def clients(): ...@@ -1478,10 +1482,30 @@ def clients():
'remote_domain': remote_domain 'remote_domain': remote_domain
}) })
# Apply search filter
if search_query:
search_pattern = search_query.lower()
clients_data = [
client for client in clients_data
if (search_pattern in client['token_name'].lower() or
search_pattern in client['username'].lower() or
search_pattern in client['rustdesk_id'].lower() or
(client['ip_address'] and search_pattern in client['ip_address'].lower()))
]
# Apply status filter
if status_filter == 'online':
clients_data = [client for client in clients_data if client['is_online']]
elif status_filter == 'offline':
clients_data = [client for client in clients_data if not client['is_online']]
# Sort: online clients first, then offline clients by last seen # Sort: online clients first, then offline clients by last seen
clients_data.sort(key=lambda x: (not x['is_online'], x['last_seen']), reverse=True) clients_data.sort(key=lambda x: (not x['is_online'], x['last_seen']), reverse=True)
return render_template('main/clients.html', clients=clients_data) return render_template('main/clients.html',
clients=clients_data,
status_filter=status_filter,
search_query=search_query)
except Exception as e: except Exception as e:
logger.error(f"Clients page error: {str(e)}") logger.error(f"Clients page error: {str(e)}")
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
{% block title %}Clients - Fixture Manager{% endblock %} {% block title %}Clients - Fixture Manager{% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1>Connected Clients</h1> <h1>Connected Clients</h1>
<div class="d-flex gap-1"> <div class="d-flex gap-1">
<span class="badge badge-success" style="background-color: #28a745; color: white;">Online</span> <span class="badge badge-success" style="background-color: #28a745; color: white;">Online</span>
...@@ -11,9 +11,46 @@ ...@@ -11,9 +11,46 @@
</div> </div>
</div> </div>
<!-- Filter and Search Controls -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<div class="input-group">
<input type="text"
class="form-control"
placeholder="Search clients by name, username, RustDesk ID, or IP..."
value="{{ search_query if search_query else '' }}"
id="searchInput">
<button class="btn btn-primary" type="button" id="searchButton">
<i class="bi bi-search"></i> Search
</button>
{% if search_query %}
<button class="btn btn-outline-secondary" type="button" id="clearSearchButton">
<i class="bi bi-x"></i> Clear
</button>
{% endif %}
</div>
</div>
<div class="col-md-4">
<select class="form-select" id="statusFilter">
<option value="">All Clients</option>
<option value="online" {% if status_filter == 'online' %}selected{% endif %}>Online Only</option>
<option value="offline" {% if status_filter == 'offline' %}selected{% endif %}>Offline Only</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-outline-primary w-100" type="button" id="resetFilterButton">
<i class="bi bi-arrow-clockwise"></i> Reset
</button>
</div>
</div>
</div>
</div>
<div class="alert alert-info"> <div class="alert alert-info">
<strong>Client Status:</strong> Clients are considered online if they've sent a request to the API in the last 30 minutes. <strong>Client Status:</strong> Clients are considered online if they've sent a request to the API in the last 30 minutes.
The list shows all clients first (online), followed by offline clients. Use the filters above to show only online clients, only offline clients, or search for specific clients by name, username, RustDesk ID, or IP address.
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
...@@ -120,5 +157,94 @@ ...@@ -120,5 +157,94 @@
border-radius: 3px; border-radius: 3px;
font-size: 0.9em; font-size: 0.9em;
} }
/* Filter card styling */
.card {
border: 1px solid #dee2e6;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.card-body {
padding: 1.5rem;
}
</style> </style>
{% endblock %} {% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Get URL parameters
const urlParams = new URLSearchParams(window.location.search);
const currentSearch = urlParams.get('search') || '';
const currentStatus = urlParams.get('status') || '';
// Set initial values
document.getElementById('searchInput').value = currentSearch;
document.getElementById('statusFilter').value = currentStatus;
// Search functionality
document.getElementById('searchButton').addEventListener('click', function() {
const searchQuery = document.getElementById('searchInput').value.trim();
const statusFilter = document.getElementById('statusFilter').value;
let url = '{{ url_for("main.clients") }}?';
if (searchQuery) {
url += 'search=' + encodeURIComponent(searchQuery) + '&';
}
if (statusFilter) {
url += 'status=' + encodeURIComponent(statusFilter) + '&';
}
// Remove trailing & or ?
url = url.replace(/[&?]$/, '');
window.location.href = url;
});
// Clear search functionality
document.getElementById('clearSearchButton').addEventListener('click', function() {
document.getElementById('searchInput').value = '';
const statusFilter = document.getElementById('statusFilter').value;
let url = '{{ url_for("main.clients") }}?';
if (statusFilter) {
url += 'status=' + encodeURIComponent(statusFilter) + '&';
}
// Remove trailing & or ?
url = url.replace(/[&?]$/, '');
window.location.href = url;
});
// Status filter change
document.getElementById('statusFilter').addEventListener('change', function() {
const searchQuery = document.getElementById('searchInput').value.trim();
const statusFilter = this.value;
let url = '{{ url_for("main.clients") }}?';
if (searchQuery) {
url += 'search=' + encodeURIComponent(searchQuery) + '&';
}
if (statusFilter) {
url += 'status=' + encodeURIComponent(statusFilter) + '&';
}
// Remove trailing & or ?
url = url.replace(/[&?]$/, '');
window.location.href = url;
});
// Reset filter functionality
document.getElementById('resetFilterButton').addEventListener('click', function() {
window.location.href = '{{ url_for("main.clients") }}';
});
// Allow Enter key for search
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('searchButton').click();
}
});
});
</script>
{% endblock %}
\ No newline at end of file
...@@ -135,6 +135,7 @@ ...@@ -135,6 +135,7 @@
<a href="{{ url_for('main.fixtures') }}">Fixtures</a> <a href="{{ url_for('main.fixtures') }}">Fixtures</a>
<a href="{{ url_for('main.uploads') }}">Uploads</a> <a href="{{ url_for('main.uploads') }}">Uploads</a>
<a href="{{ url_for('main.statistics') }}">Statistics</a> <a href="{{ url_for('main.statistics') }}">Statistics</a>
<a href="{{ url_for('main.clients') }}">Clients</a>
<a href="{{ url_for('main.user_tokens') }}">API Tokens</a> <a href="{{ url_for('main.user_tokens') }}">API Tokens</a>
{% if current_user.is_admin %} {% if current_user.is_admin %}
<a href="{{ url_for('main.admin_panel') }}">Admin</a> <a href="{{ url_for('main.admin_panel') }}">Admin</a>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment