Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
M
MBetterd
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Mbetter
MBetterd
Commits
4b1d26d2
Commit
4b1d26d2
authored
Jan 12, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Better page for clients
parent
005f21b5
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
167 additions
and
16 deletions
+167
-16
routes.py
app/main/routes.py
+35
-11
clients.html
app/templates/main/clients.html
+131
-5
dashboard.html
app/templates/main/dashboard.html
+1
-0
No files found.
app/main/routes.py
View file @
4b1d26d2
...
@@ -1419,14 +1419,18 @@ def download_zip(match_id):
...
@@ -1419,14 +1419,18 @@ 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'
)
# Get clients with their associated token and user info
# Get clients with their associated token and user info
clients_query
=
db
.
session
.
query
(
clients_query
=
db
.
session
.
query
(
ClientActivity
,
ClientActivity
,
...
@@ -1440,21 +1444,21 @@ def clients():
...
@@ -1440,21 +1444,21 @@ def clients():
)
.
order_by
(
)
.
order_by
(
ClientActivity
.
last_seen
.
desc
()
ClientActivity
.
last_seen
.
desc
()
)
)
clients_data
=
[]
clients_data
=
[]
for
client_activity
,
token_name
,
user_id
,
token_created_at
in
clients_query
.
all
():
for
client_activity
,
token_name
,
user_id
,
token_created_at
in
clients_query
.
all
():
# Get user info
# Get user info
from
app.models
import
User
from
app.models
import
User
user
=
User
.
query
.
get
(
user_id
)
user
=
User
.
query
.
get
(
user_id
)
# Calculate if client is online (last seen within 30 minutes)
# Calculate if client is online (last seen within 30 minutes)
now
=
datetime
.
utcnow
()
now
=
datetime
.
utcnow
()
time_diff
=
now
-
client_activity
.
last_seen
time_diff
=
now
-
client_activity
.
last_seen
is_online
=
time_diff
.
total_seconds
()
<=
1800
# 30 minutes = 1800 seconds
is_online
=
time_diff
.
total_seconds
()
<=
1800
# 30 minutes = 1800 seconds
# Format last seen time
# Format last seen time
last_seen_formatted
=
client_activity
.
last_seen
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
last_seen_formatted
=
client_activity
.
last_seen
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
# Calculate time ago
# Calculate time ago
if
time_diff
.
total_seconds
()
<
60
:
if
time_diff
.
total_seconds
()
<
60
:
last_seen_ago
=
f
"{int(time_diff.total_seconds())} seconds ago"
last_seen_ago
=
f
"{int(time_diff.total_seconds())} seconds ago"
...
@@ -1464,7 +1468,7 @@ def clients():
...
@@ -1464,7 +1468,7 @@ def clients():
last_seen_ago
=
f
"{int(time_diff.total_seconds() / 3600)} hours ago"
last_seen_ago
=
f
"{int(time_diff.total_seconds() / 3600)} hours ago"
else
:
else
:
last_seen_ago
=
f
"{int(time_diff.total_seconds() / 86400)} days ago"
last_seen_ago
=
f
"{int(time_diff.total_seconds() / 86400)} days ago"
clients_data
.
append
({
clients_data
.
append
({
'rustdesk_id'
:
client_activity
.
rustdesk_id
,
'rustdesk_id'
:
client_activity
.
rustdesk_id
,
'token_name'
:
token_name
,
'token_name'
:
token_name
,
...
@@ -1477,11 +1481,31 @@ def clients():
...
@@ -1477,11 +1481,31 @@ def clients():
'user_agent'
:
client_activity
.
user_agent
,
'user_agent'
:
client_activity
.
user_agent
,
'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)}"
)
...
...
app/templates/main/clients.html
View file @
4b1d26d2
...
@@ -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 client
s.
Use the filters above to show only online clients, only offline clients, or search for specific clients by name, username, RustDesk ID, or IP addres
s.
</div>
</div>
<div
class=
"table-responsive"
>
<div
class=
"table-responsive"
>
...
@@ -105,20 +142,109 @@
...
@@ -105,20 +142,109 @@
font-size
:
0.8rem
;
font-size
:
0.8rem
;
font-weight
:
bold
;
font-weight
:
bold
;
}
}
.table-success
{
.table-success
{
background-color
:
rgba
(
40
,
167
,
69
,
0.1
)
!important
;
background-color
:
rgba
(
40
,
167
,
69
,
0.1
)
!important
;
}
}
.table-secondary
{
.table-secondary
{
background-color
:
rgba
(
108
,
117
,
125
,
0.1
)
!important
;
background-color
:
rgba
(
108
,
117
,
125
,
0.1
)
!important
;
}
}
code
{
code
{
background-color
:
#f8f9fa
;
background-color
:
#f8f9fa
;
padding
:
2px
4px
;
padding
:
2px
4px
;
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 %}
{% 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 %}
{% endblock %}
\ No newline at end of file
app/templates/main/dashboard.html
View file @
4b1d26d2
...
@@ -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>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment