Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
M
MBetterc
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
MBetterc
Commits
e806a118
Commit
e806a118
authored
Nov 24, 2025
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Almost there!
parent
e8a91c81
Changes
13
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
1797 additions
and
376 deletions
+1797
-376
games_thread.py
mbetterclient/core/games_thread.py
+408
-47
match_timer.py
mbetterclient/core/match_timer.py
+58
-17
message_bus.py
mbetterclient/core/message_bus.py
+26
-6
udp_broadcast.py
mbetterclient/core/udp_broadcast.py
+52
-22
.player.py.swn
mbetterclient/qt_player/.player.py.swn
+0
-0
player.py
mbetterclient/qt_player/player.py
+371
-62
match_video.html
mbetterclient/qt_player/templates/match_video.html
+392
-60
results.html
mbetterclient/qt_player/templates/results.html
+416
-105
app.py
mbetterclient/web_dashboard/app.py
+58
-22
routes.py
mbetterclient/web_dashboard/routes.py
+13
-35
cashier_verify_bet.html
...web_dashboard/templates/dashboard/cashier_verify_bet.html
+1
-0
verify_bet.html
...rclient/web_dashboard/templates/dashboard/verify_bet.html
+1
-0
verify_bet_mobile.html
.../web_dashboard/templates/dashboard/verify_bet_mobile.html
+1
-0
No files found.
mbetterclient/core/games_thread.py
View file @
e806a118
This diff is collapsed.
Click to expand it.
mbetterclient/core/match_timer.py
View file @
e806a118
...
@@ -31,6 +31,7 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -31,6 +31,7 @@ class MatchTimerComponent(ThreadedComponent):
self
.
timer_duration_seconds
=
0
self
.
timer_duration_seconds
=
0
self
.
current_fixture_id
:
Optional
[
str
]
=
None
self
.
current_fixture_id
:
Optional
[
str
]
=
None
self
.
current_match_id
:
Optional
[
int
]
=
None
self
.
current_match_id
:
Optional
[
int
]
=
None
self
.
pending_match_id
:
Optional
[
int
]
=
None
# Match prepared by START_INTRO
# Synchronization
# Synchronization
self
.
_timer_lock
=
threading
.
RLock
()
self
.
_timer_lock
=
threading
.
RLock
()
...
@@ -44,6 +45,7 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -44,6 +45,7 @@ class MatchTimerComponent(ThreadedComponent):
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
SCHEDULE_GAMES
,
self
.
_handle_schedule_games
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
SCHEDULE_GAMES
,
self
.
_handle_schedule_games
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
CUSTOM
,
self
.
_handle_custom_message
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
CUSTOM
,
self
.
_handle_custom_message
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
NEXT_MATCH
,
self
.
_handle_next_match
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
NEXT_MATCH
,
self
.
_handle_next_match
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
START_INTRO
,
self
.
_handle_start_intro
)
logger
.
info
(
"MatchTimer component initialized"
)
logger
.
info
(
"MatchTimer component initialized"
)
...
@@ -114,6 +116,8 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -114,6 +116,8 @@ class MatchTimerComponent(ThreadedComponent):
self
.
_handle_custom_message
(
message
)
self
.
_handle_custom_message
(
message
)
elif
message
.
type
==
MessageType
.
NEXT_MATCH
:
elif
message
.
type
==
MessageType
.
NEXT_MATCH
:
self
.
_handle_next_match
(
message
)
self
.
_handle_next_match
(
message
)
elif
message
.
type
==
MessageType
.
START_INTRO
:
self
.
_handle_start_intro
(
message
)
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to process message: {e}"
)
logger
.
error
(
f
"Failed to process message: {e}"
)
...
@@ -125,6 +129,7 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -125,6 +129,7 @@ class MatchTimerComponent(ThreadedComponent):
self
.
timer_start_time
=
None
self
.
timer_start_time
=
None
self
.
current_fixture_id
=
None
self
.
current_fixture_id
=
None
self
.
current_match_id
=
None
self
.
current_match_id
=
None
self
.
pending_match_id
=
None
# Unregister from message bus
# Unregister from message bus
self
.
message_bus
.
unregister_component
(
self
.
name
)
self
.
message_bus
.
unregister_component
(
self
.
name
)
...
@@ -229,12 +234,13 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -229,12 +234,13 @@ class MatchTimerComponent(ThreadedComponent):
logger
.
error
(
f
"Failed to handle custom message: {e}"
)
logger
.
error
(
f
"Failed to handle custom message: {e}"
)
def
_handle_next_match
(
self
,
message
:
Message
):
def
_handle_next_match
(
self
,
message
:
Message
):
"""Handle NEXT_MATCH message -
start the next match in sequence
"""
"""Handle NEXT_MATCH message -
restart timer for next match interval
"""
try
:
try
:
fixture_id
=
message
.
data
.
get
(
"fixture_id"
)
fixture_id
=
message
.
data
.
get
(
"fixture_id"
)
match_id
=
message
.
data
.
get
(
"match_id"
)
match_id
=
message
.
data
.
get
(
"match_id"
)
logger
.
info
(
f
"Received NEXT_MATCH message for fixture {fixture_id}, match {match_id}"
)
logger
.
info
(
f
"Received NEXT_MATCH message for fixture {fixture_id}, match {match_id}"
)
logger
.
info
(
"Previous match completed - restarting timer for next interval"
)
# Find and start the next match
# Find and start the next match
match_info
=
self
.
_find_and_start_next_match
()
match_info
=
self
.
_find_and_start_next_match
()
...
@@ -245,12 +251,37 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -245,12 +251,37 @@ class MatchTimerComponent(ThreadedComponent):
# Reset timer for next interval
# Reset timer for next interval
match_interval
=
self
.
_get_match_interval
()
match_interval
=
self
.
_get_match_interval
()
self
.
_start_timer
(
match_interval
*
60
,
match_info
[
'fixture_id'
])
self
.
_start_timer
(
match_interval
*
60
,
match_info
[
'fixture_id'
])
logger
.
info
(
f
"Timer restarted for {match_interval} minute interval"
)
else
:
else
:
logger
.
info
(
"No more matches to start, stopping timer"
)
logger
.
info
(
"No more matches to start, stopping timer"
)
self
.
_stop_timer
()
self
.
_stop_timer
()
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to handle NEXT_MATCH message: {e}"
)
logger
.
error
(
f
"Failed to handle NEXT_MATCH message: {e}"
)
# On error, try to restart timer anyway
try
:
match_interval
=
self
.
_get_match_interval
()
self
.
_start_timer
(
match_interval
*
60
,
self
.
current_fixture_id
)
except
Exception
as
restart_e
:
logger
.
error
(
f
"Failed to restart timer after NEXT_MATCH error: {restart_e}"
)
def
_handle_start_intro
(
self
,
message
:
Message
):
"""Handle START_INTRO message - store the match_id for later MATCH_START"""
try
:
fixture_id
=
message
.
data
.
get
(
"fixture_id"
)
match_id
=
message
.
data
.
get
(
"match_id"
)
logger
.
info
(
f
"Received START_INTRO message for fixture {fixture_id}, match {match_id}"
)
# Store the match_id for when timer expires
with
self
.
_timer_lock
:
self
.
pending_match_id
=
match_id
self
.
current_fixture_id
=
fixture_id
logger
.
info
(
f
"Stored pending match_id {match_id} for timer expiration"
)
except
Exception
as
e
:
logger
.
error
(
f
"Failed to handle START_INTRO message: {e}"
)
def
_start_timer
(
self
,
duration_seconds
:
int
,
fixture_id
:
Optional
[
str
]):
def
_start_timer
(
self
,
duration_seconds
:
int
,
fixture_id
:
Optional
[
str
]):
"""Start the countdown timer"""
"""Start the countdown timer"""
...
@@ -279,28 +310,38 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -279,28 +310,38 @@ class MatchTimerComponent(ThreadedComponent):
self
.
_send_timer_update
()
self
.
_send_timer_update
()
def
_on_timer_expired
(
self
):
def
_on_timer_expired
(
self
):
"""Handle timer expiration - start
next match
"""
"""Handle timer expiration - start
the match that was prepared by START_INTRO
"""
try
:
try
:
logger
.
info
(
"Match timer expired, starting
next
match..."
)
logger
.
info
(
"Match timer expired, starting
prepared
match..."
)
# Find and start the next match
with
self
.
_timer_lock
:
match_info
=
self
.
_find_and_start_next_match
()
pending_match_id
=
self
.
pending_match_id
fixture_id
=
self
.
current_fixture_id
if
match_info
:
if
pending_match_id
:
logger
.
info
(
f
"Started match {match_info['match_id']} in fixture {match_info['fixture_id']}"
)
# Send MATCH_START message for the prepared match
match_start_message
=
MessageBuilder
.
match_start
(
sender
=
self
.
name
,
fixture_id
=
fixture_id
,
match_id
=
pending_match_id
)
# Reset timer for next interval
self
.
message_bus
.
publish
(
match_start_message
)
match_interval
=
self
.
_get_match_interval
()
logger
.
info
(
f
"Sent MATCH_START for prepared match {pending_match_id} in fixture {fixture_id}"
)
self
.
_start_timer
(
match_interval
*
60
,
match_info
[
'fixture_id'
])
logger
.
info
(
"Timer stopped - will restart after match completion (NEXT_MATCH)"
)
# Clear the pending match and stop the timer
with
self
.
_timer_lock
:
self
.
pending_match_id
=
None
self
.
_stop_timer
()
else
:
else
:
logger
.
info
(
"No more matches to start
, stopping timer"
)
logger
.
warning
(
"No pending match prepared by START_INTRO
, stopping timer"
)
self
.
_stop_timer
()
self
.
_stop_timer
()
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to handle timer expiration: {e}"
)
logger
.
error
(
f
"Failed to handle timer expiration: {e}"
)
# Reset timer on error
# Stop timer on error - don't restart automatically
match_interval
=
self
.
_get_match_interval
()
self
.
_stop_timer
()
self
.
_start_timer
(
match_interval
*
60
,
self
.
current_fixture_id
)
def
_find_and_start_next_match
(
self
)
->
Optional
[
Dict
[
str
,
Any
]]:
def
_find_and_start_next_match
(
self
)
->
Optional
[
Dict
[
str
,
Any
]]:
"""Find and start the next available match"""
"""Find and start the next available match"""
...
@@ -355,14 +396,14 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -355,14 +396,14 @@ class MatchTimerComponent(ThreadedComponent):
target_fixture_id
=
target_match
.
fixture_id
target_fixture_id
=
target_match
.
fixture_id
if
target_match
:
if
target_match
:
# Send
MATCH_START
message
# Send
START_INTRO
message
match_start_message
=
MessageBuilder
.
match_start
(
start_intro_message
=
MessageBuilder
.
start_intro
(
sender
=
self
.
name
,
sender
=
self
.
name
,
fixture_id
=
target_fixture_id
or
target_match
.
fixture_id
,
fixture_id
=
target_fixture_id
or
target_match
.
fixture_id
,
match_id
=
target_match
.
id
match_id
=
target_match
.
id
)
)
self
.
message_bus
.
publish
(
match_start
_message
)
self
.
message_bus
.
publish
(
start_intro
_message
)
return
{
return
{
"fixture_id"
:
target_fixture_id
or
target_match
.
fixture_id
,
"fixture_id"
:
target_fixture_id
or
target_match
.
fixture_id
,
...
...
mbetterclient/core/message_bus.py
View file @
e806a118
...
@@ -70,6 +70,7 @@ class MessageType(Enum):
...
@@ -70,6 +70,7 @@ class MessageType(Enum):
PLAY_VIDEO_MATCH
=
"PLAY_VIDEO_MATCH"
PLAY_VIDEO_MATCH
=
"PLAY_VIDEO_MATCH"
PLAY_VIDEO_MATCH_DONE
=
"PLAY_VIDEO_MATCH_DONE"
PLAY_VIDEO_MATCH_DONE
=
"PLAY_VIDEO_MATCH_DONE"
PLAY_VIDEO_RESULT
=
"PLAY_VIDEO_RESULT"
PLAY_VIDEO_RESULT
=
"PLAY_VIDEO_RESULT"
PLAY_VIDEO_RESULT_DONE
=
"PLAY_VIDEO_RESULT_DONE"
MATCH_DONE
=
"MATCH_DONE"
MATCH_DONE
=
"MATCH_DONE"
NEXT_MATCH
=
"NEXT_MATCH"
NEXT_MATCH
=
"NEXT_MATCH"
GAME_STATUS
=
"GAME_STATUS"
GAME_STATUS
=
"GAME_STATUS"
...
@@ -661,11 +662,27 @@ class MessageBuilder:
...
@@ -661,11 +662,27 @@ class MessageBuilder:
)
)
@
staticmethod
@
staticmethod
def
play_video_result
(
sender
:
str
,
fixture_id
:
str
,
match_id
:
int
,
result
:
str
)
->
Message
:
def
play_video_result
(
sender
:
str
,
fixture_id
:
str
,
match_id
:
int
,
result
:
str
,
under_over_result
:
Optional
[
str
]
=
None
)
->
Message
:
"""Create PLAY_VIDEO_RESULT message"""
"""Create PLAY_VIDEO_RESULT message"""
data
=
{
"fixture_id"
:
fixture_id
,
"match_id"
:
match_id
,
"result"
:
result
}
if
under_over_result
is
not
None
:
data
[
"under_over_result"
]
=
under_over_result
return
Message
(
return
Message
(
type
=
MessageType
.
PLAY_VIDEO_RESULT
,
type
=
MessageType
.
PLAY_VIDEO_RESULT
,
sender
=
sender
,
sender
=
sender
,
data
=
data
)
@
staticmethod
def
play_video_result_done
(
sender
:
str
,
fixture_id
:
str
,
match_id
:
int
,
result
:
str
)
->
Message
:
"""Create PLAY_VIDEO_RESULT_DONE message"""
return
Message
(
type
=
MessageType
.
PLAY_VIDEO_RESULT_DONE
,
sender
=
sender
,
data
=
{
data
=
{
"fixture_id"
:
fixture_id
,
"fixture_id"
:
fixture_id
,
"match_id"
:
match_id
,
"match_id"
:
match_id
,
...
@@ -674,15 +691,18 @@ class MessageBuilder:
...
@@ -674,15 +691,18 @@ class MessageBuilder:
)
)
@
staticmethod
@
staticmethod
def
match_done
(
sender
:
str
,
fixture_id
:
str
,
match_id
:
int
)
->
Message
:
def
match_done
(
sender
:
str
,
fixture_id
:
str
,
match_id
:
int
,
result
:
Optional
[
str
]
=
None
)
->
Message
:
"""Create MATCH_DONE message"""
"""Create MATCH_DONE message"""
data
=
{
"fixture_id"
:
fixture_id
,
"match_id"
:
match_id
}
if
result
is
not
None
:
data
[
"result"
]
=
result
return
Message
(
return
Message
(
type
=
MessageType
.
MATCH_DONE
,
type
=
MessageType
.
MATCH_DONE
,
sender
=
sender
,
sender
=
sender
,
data
=
{
data
=
data
"fixture_id"
:
fixture_id
,
"match_id"
:
match_id
}
)
)
@
staticmethod
@
staticmethod
...
...
mbetterclient/core/udp_broadcast.py
View file @
e806a118
...
@@ -24,7 +24,7 @@ class UDPBroadcastComponent(ThreadedComponent):
...
@@ -24,7 +24,7 @@ class UDPBroadcastComponent(ThreadedComponent):
self
.
broadcast_port
=
broadcast_port
self
.
broadcast_port
=
broadcast_port
self
.
broadcast_interval
=
30.0
# 30 seconds
self
.
broadcast_interval
=
30.0
# 30 seconds
self
.
socket
:
Optional
[
socket
.
socket
]
=
None
self
.
socket
:
Optional
[
socket
.
socket
]
=
None
# Server information to broadcast
# Server information to broadcast
self
.
server_info
:
Dict
[
str
,
Any
]
=
{
self
.
server_info
:
Dict
[
str
,
Any
]
=
{
"service"
:
"MBetterClient"
,
"service"
:
"MBetterClient"
,
...
@@ -34,10 +34,13 @@ class UDPBroadcastComponent(ThreadedComponent):
...
@@ -34,10 +34,13 @@ class UDPBroadcastComponent(ThreadedComponent):
"url"
:
"http://127.0.0.1:5001"
,
"url"
:
"http://127.0.0.1:5001"
,
"timestamp"
:
time
.
time
()
"timestamp"
:
time
.
time
()
}
}
# Shutdown event for responsive shutdown
self
.
shutdown_event
=
threading
.
Event
()
# Register message queue
# Register message queue
self
.
message_queue
=
self
.
message_bus
.
register_component
(
self
.
name
)
self
.
message_queue
=
self
.
message_bus
.
register_component
(
self
.
name
)
logger
.
info
(
f
"UDP Broadcast component initialized on port {broadcast_port}"
)
logger
.
info
(
f
"UDP Broadcast component initialized on port {broadcast_port}"
)
def
initialize
(
self
)
->
bool
:
def
initialize
(
self
)
->
bool
:
...
@@ -47,13 +50,18 @@ class UDPBroadcastComponent(ThreadedComponent):
...
@@ -47,13 +50,18 @@ class UDPBroadcastComponent(ThreadedComponent):
self
.
socket
=
socket
.
socket
(
socket
.
AF_INET
,
socket
.
SOCK_DGRAM
)
self
.
socket
=
socket
.
socket
(
socket
.
AF_INET
,
socket
.
SOCK_DGRAM
)
self
.
socket
.
setsockopt
(
socket
.
SOL_SOCKET
,
socket
.
SO_BROADCAST
,
1
)
self
.
socket
.
setsockopt
(
socket
.
SOL_SOCKET
,
socket
.
SO_BROADCAST
,
1
)
self
.
socket
.
setsockopt
(
socket
.
SOL_SOCKET
,
socket
.
SO_REUSEADDR
,
1
)
self
.
socket
.
setsockopt
(
socket
.
SOL_SOCKET
,
socket
.
SO_REUSEADDR
,
1
)
# Set timeout to prevent blocking operations
self
.
socket
.
settimeout
(
1.0
)
# Clear shutdown event
self
.
shutdown_event
.
clear
()
# Subscribe to system status messages to get web server info
# Subscribe to system status messages to get web server info
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
SYSTEM_STATUS
,
self
.
_handle_system_status
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
SYSTEM_STATUS
,
self
.
_handle_system_status
)
logger
.
info
(
"UDP broadcast socket initialized successfully"
)
logger
.
info
(
"UDP broadcast socket initialized successfully"
)
return
True
return
True
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"UDP broadcast initialization failed: {e}"
)
logger
.
error
(
f
"UDP broadcast initialization failed: {e}"
)
return
False
return
False
...
@@ -68,28 +76,33 @@ class UDPBroadcastComponent(ThreadedComponent):
...
@@ -68,28 +76,33 @@ class UDPBroadcastComponent(ThreadedComponent):
last_broadcast_time
=
0
last_broadcast_time
=
0
while
self
.
running
:
while
self
.
running
and
not
self
.
shutdown_event
.
is_set
()
:
try
:
try
:
current_time
=
time
.
time
()
current_time
=
time
.
time
()
# Process messages
# Process messages
with shorter timeout for responsive shutdown
message
=
self
.
message_bus
.
get_message
(
self
.
name
,
timeout
=
1.0
)
message
=
self
.
message_bus
.
get_message
(
self
.
name
,
timeout
=
0.5
)
if
message
:
if
message
:
self
.
_process_message
(
message
)
self
.
_process_message
(
message
)
# Broadcast every 30 seconds
# Broadcast every 30 seconds
if
current_time
-
last_broadcast_time
>=
self
.
broadcast_interval
:
if
current_time
-
last_broadcast_time
>=
self
.
broadcast_interval
:
self
.
_broadcast_server_info
()
self
.
_broadcast_server_info
()
last_broadcast_time
=
current_time
last_broadcast_time
=
current_time
# Update heartbeat
# Update heartbeat
self
.
heartbeat
()
self
.
heartbeat
()
time
.
sleep
(
0.5
)
# Check shutdown event more frequently
if
self
.
shutdown_event
.
wait
(
0.5
):
break
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"UDP broadcast loop error: {e}"
)
logger
.
error
(
f
"UDP broadcast loop error: {e}"
)
time
.
sleep
(
2.0
)
# Shorter sleep on error to be more responsive to shutdown
if
not
self
.
shutdown_event
.
wait
(
1.0
):
continue
break
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"UDP broadcast run failed: {e}"
)
logger
.
error
(
f
"UDP broadcast run failed: {e}"
)
...
@@ -100,11 +113,19 @@ class UDPBroadcastComponent(ThreadedComponent):
...
@@ -100,11 +113,19 @@ class UDPBroadcastComponent(ThreadedComponent):
"""Shutdown UDP broadcast component"""
"""Shutdown UDP broadcast component"""
try
:
try
:
logger
.
info
(
"Shutting down UDP broadcast..."
)
logger
.
info
(
"Shutting down UDP broadcast..."
)
# Signal shutdown event to wake up the main loop
self
.
shutdown_event
.
set
()
# Close socket to prevent further operations
if
self
.
socket
:
if
self
.
socket
:
self
.
socket
.
close
()
try
:
self
.
socket
=
None
self
.
socket
.
close
()
except
Exception
as
e
:
logger
.
debug
(
f
"Error closing UDP socket: {e}"
)
finally
:
self
.
socket
=
None
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"UDP broadcast shutdown error: {e}"
)
logger
.
error
(
f
"UDP broadcast shutdown error: {e}"
)
...
@@ -170,8 +191,17 @@ class UDPBroadcastComponent(ThreadedComponent):
...
@@ -170,8 +191,17 @@ class UDPBroadcastComponent(ThreadedComponent):
for
broadcast_addr
in
broadcast_addresses
:
for
broadcast_addr
in
broadcast_addresses
:
try
:
try
:
self
.
socket
.
sendto
(
broadcast_data
,
(
broadcast_addr
,
self
.
broadcast_port
))
if
self
.
socket
and
not
self
.
shutdown_event
.
is_set
():
logger
.
debug
(
f
"Broadcasted to {broadcast_addr}:{self.broadcast_port}"
)
self
.
socket
.
sendto
(
broadcast_data
,
(
broadcast_addr
,
self
.
broadcast_port
))
logger
.
debug
(
f
"Broadcasted to {broadcast_addr}:{self.broadcast_port}"
)
else
:
break
# Exit if shutting down or socket closed
except
socket
.
timeout
:
logger
.
debug
(
f
"Timeout broadcasting to {broadcast_addr}"
)
except
OSError
as
e
:
if
self
.
shutdown_event
.
is_set
():
break
# Exit if shutting down
logger
.
debug
(
f
"Failed to broadcast to {broadcast_addr}: {e}"
)
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
debug
(
f
"Failed to broadcast to {broadcast_addr}: {e}"
)
logger
.
debug
(
f
"Failed to broadcast to {broadcast_addr}: {e}"
)
...
...
mbetterclient/qt_player/.player.py.swn
deleted
100644 → 0
View file @
e8a91c81
File deleted
mbetterclient/qt_player/player.py
View file @
e806a118
This diff is collapsed.
Click to expand it.
mbetterclient/qt_player/templates/match_video.html
View file @
e806a118
This diff is collapsed.
Click to expand it.
mbetterclient/qt_player/templates/results.html
View file @
e806a118
This diff is collapsed.
Click to expand it.
mbetterclient/web_dashboard/app.py
View file @
e806a118
...
@@ -351,15 +351,52 @@ class WebDashboard(ThreadedComponent):
...
@@ -351,15 +351,52 @@ class WebDashboard(ThreadedComponent):
def
_create_server
(
self
):
def
_create_server
(
self
):
"""Create HTTP/HTTPS server with SocketIO support"""
"""Create HTTP/HTTPS server with SocketIO support"""
try
:
try
:
from
werkzeug.serving
import
make_server
protocol
=
"HTTP"
protocol
=
"HTTP"
if
self
.
settings
.
enable_ssl
:
if
self
.
settings
.
enable_ssl
:
protocol
=
"HTTPS"
protocol
=
"HTTPS"
logger
.
info
(
"SSL enabled - SocketIO server will use HTTPS"
)
logger
.
info
(
"SSL enabled - server will use HTTPS"
)
# Get SSL certificate paths
from
..config.settings
import
get_user_data_dir
cert_path
,
key_path
=
get_ssl_certificate_paths
(
get_user_data_dir
())
if
cert_path
and
key_path
:
# Create SSL context for HTTPS
self
.
ssl_context
=
create_ssl_context
(
cert_path
,
key_path
)
if
not
self
.
ssl_context
:
logger
.
warning
(
"SSL context creation failed, falling back to HTTP"
)
self
.
ssl_context
=
None
protocol
=
"HTTP"
else
:
logger
.
warning
(
"SSL certificate files not available, falling back to HTTP"
)
self
.
ssl_context
=
None
protocol
=
"HTTP"
# Create WSGI server that can be shutdown
if
self
.
socketio
:
# For SocketIO, try to use the SocketIO WSGI app
try
:
wsgi_app
=
self
.
socketio
.
WSGIApp
(
self
.
app
)
except
AttributeError
:
# Fallback for older SocketIO versions or different implementations
logger
.
warning
(
"SocketIO WSGIApp not available, falling back to standard Flask app"
)
wsgi_app
=
self
.
app
self
.
socketio
=
None
# Disable SocketIO since we can't use it
else
:
wsgi_app
=
self
.
app
self
.
server
=
make_server
(
host
=
self
.
settings
.
host
,
port
=
self
.
settings
.
port
,
app
=
wsgi_app
,
threaded
=
True
,
ssl_context
=
self
.
ssl_context
)
logger
.
info
(
f
"{protocol} server
with SocketIO
created on {self.settings.host}:{self.settings.port}"
)
logger
.
info
(
f
"{protocol} server created on {self.settings.host}:{self.settings.port}"
)
if
self
.
settings
.
enable_ssl
:
if
self
.
settings
.
enable_ssl
and
self
.
ssl_context
:
logger
.
info
(
"⚠️ Using self-signed certificate - browsers will show security warning"
)
logger
.
info
(
"⚠️ Using self-signed certificate - browsers will show security warning"
)
logger
.
info
(
" You can safely proceed by accepting the certificate"
)
logger
.
info
(
" You can safely proceed by accepting the certificate"
)
...
@@ -427,29 +464,22 @@ class WebDashboard(ThreadedComponent):
...
@@ -427,29 +464,22 @@ class WebDashboard(ThreadedComponent):
socketio_status
=
"with SocketIO"
if
self
.
socketio
else
"without SocketIO"
socketio_status
=
"with SocketIO"
if
self
.
socketio
else
"without SocketIO"
logger
.
info
(
f
"Starting {protocol} server {socketio_status} on {self.settings.host}:{self.settings.port}"
)
logger
.
info
(
f
"Starting {protocol} server {socketio_status} on {self.settings.host}:{self.settings.port}"
)
if
self
.
socketio
:
if
self
.
server
:
# Run SocketIO server
# Use the shutdown-capable server
self
.
socketio
.
run
(
# serve_forever() will block until shutdown() is called
self
.
app
,
self
.
server
.
serve_forever
()
host
=
self
.
settings
.
host
,
logger
.
info
(
"HTTP server stopped"
)
port
=
self
.
settings
.
port
,
debug
=
False
,
use_reloader
=
False
,
log_output
=
False
)
else
:
else
:
# Run Flask server without SocketIO
logger
.
error
(
"Server not created, cannot start"
)
self
.
app
.
run
(
return
host
=
self
.
settings
.
host
,
port
=
self
.
settings
.
port
,
debug
=
False
,
use_reloader
=
False
)
except
Exception
as
e
:
except
Exception
as
e
:
if
self
.
running
:
# Only log if not shutting down
if
self
.
running
:
# Only log if not shutting down
protocol
=
"HTTPS"
if
self
.
settings
.
enable_ssl
else
"HTTP"
protocol
=
"HTTPS"
if
self
.
settings
.
enable_ssl
else
"HTTP"
logger
.
error
(
f
"{protocol} server error: {e}"
)
logger
.
error
(
f
"{protocol} server error: {e}"
)
else
:
# Expected during shutdown
logger
.
debug
(
f
"Server stopped during shutdown: {e}"
)
def
_setup_ssl_error_suppression
(
self
):
def
_setup_ssl_error_suppression
(
self
):
"""Setup logging filter to suppress expected SSL connection errors"""
"""Setup logging filter to suppress expected SSL connection errors"""
...
@@ -492,10 +522,16 @@ class WebDashboard(ThreadedComponent):
...
@@ -492,10 +522,16 @@ class WebDashboard(ThreadedComponent):
"""Shutdown web dashboard"""
"""Shutdown web dashboard"""
try
:
try
:
logger
.
info
(
"Shutting down WebDashboard..."
)
logger
.
info
(
"Shutting down WebDashboard..."
)
# Shutdown the HTTP server
if
self
.
server
:
if
self
.
server
:
logger
.
info
(
"Shutting down HTTP server..."
)
self
.
server
.
shutdown
()
self
.
server
.
shutdown
()
logger
.
info
(
"HTTP server shutdown initiated"
)
# Note: SocketIO connections will be closed when the server shuts down
# No explicit SocketIO shutdown needed as it's handled by the WSGI server
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"WebDashboard shutdown error: {e}"
)
logger
.
error
(
f
"WebDashboard shutdown error: {e}"
)
...
...
mbetterclient/web_dashboard/routes.py
View file @
e806a118
...
@@ -410,17 +410,9 @@ def api_tokens():
...
@@ -410,17 +410,9 @@ def api_tokens():
def
fixtures
():
def
fixtures
():
"""Fixtures management page"""
"""Fixtures management page"""
try
:
try
:
# Restrict cashier users from accessing fixtures page
if
hasattr
(
current_user
,
'role'
)
and
current_user
.
role
==
'cashier'
:
flash
(
"Access denied"
,
"error"
)
return
redirect
(
url_for
(
'main.cashier_dashboard'
))
elif
hasattr
(
current_user
,
'is_cashier_user'
)
and
current_user
.
is_cashier_user
():
flash
(
"Access denied"
,
"error"
)
return
redirect
(
url_for
(
'main.cashier_dashboard'
))
return
render_template
(
'dashboard/fixtures.html'
,
return
render_template
(
'dashboard/fixtures.html'
,
user
=
current_user
,
user
=
current_user
,
page_title
=
"Fixtures"
)
page_title
=
"Fixtures"
)
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Fixtures page error: {e}"
)
logger
.
error
(
f
"Fixtures page error: {e}"
)
flash
(
"Error loading fixtures"
,
"error"
)
flash
(
"Error loading fixtures"
,
"error"
)
...
@@ -432,18 +424,10 @@ def fixtures():
...
@@ -432,18 +424,10 @@ def fixtures():
def
fixture_details
(
fixture_id
):
def
fixture_details
(
fixture_id
):
"""Fixture details page showing all matches in the fixture"""
"""Fixture details page showing all matches in the fixture"""
try
:
try
:
# Restrict cashier users from accessing fixture details page
if
hasattr
(
current_user
,
'role'
)
and
current_user
.
role
==
'cashier'
:
flash
(
"Access denied"
,
"error"
)
return
redirect
(
url_for
(
'main.cashier_dashboard'
))
elif
hasattr
(
current_user
,
'is_cashier_user'
)
and
current_user
.
is_cashier_user
():
flash
(
"Access denied"
,
"error"
)
return
redirect
(
url_for
(
'main.cashier_dashboard'
))
return
render_template
(
'dashboard/fixture_details.html'
,
return
render_template
(
'dashboard/fixture_details.html'
,
user
=
current_user
,
user
=
current_user
,
fixture_id
=
fixture_id
,
fixture_id
=
fixture_id
,
page_title
=
f
"Fixture Details - Fixture #{fixture_id}"
)
page_title
=
f
"Fixture Details - Fixture #{fixture_id}"
)
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Fixture details page error: {e}"
)
logger
.
error
(
f
"Fixture details page error: {e}"
)
flash
(
"Error loading fixture details"
,
"error"
)
flash
(
"Error loading fixture details"
,
"error"
)
...
@@ -455,19 +439,11 @@ def fixture_details(fixture_id):
...
@@ -455,19 +439,11 @@ def fixture_details(fixture_id):
def
match_details
(
match_id
,
fixture_id
):
def
match_details
(
match_id
,
fixture_id
):
"""Match details page showing match information and outcomes"""
"""Match details page showing match information and outcomes"""
try
:
try
:
# Restrict cashier users from accessing match details page
if
hasattr
(
current_user
,
'role'
)
and
current_user
.
role
==
'cashier'
:
flash
(
"Access denied"
,
"error"
)
return
redirect
(
url_for
(
'main.cashier_dashboard'
))
elif
hasattr
(
current_user
,
'is_cashier_user'
)
and
current_user
.
is_cashier_user
():
flash
(
"Access denied"
,
"error"
)
return
redirect
(
url_for
(
'main.cashier_dashboard'
))
return
render_template
(
'dashboard/match_details.html'
,
return
render_template
(
'dashboard/match_details.html'
,
user
=
current_user
,
user
=
current_user
,
match_id
=
match_id
,
match_id
=
match_id
,
fixture_id
=
fixture_id
,
fixture_id
=
fixture_id
,
page_title
=
f
"Match Details - Match #{match_id}"
)
page_title
=
f
"Match Details - Match #{match_id}"
)
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Match details page error: {e}"
)
logger
.
error
(
f
"Match details page error: {e}"
)
flash
(
"Error loading match details"
,
"error"
)
flash
(
"Error loading match details"
,
"error"
)
...
@@ -4222,7 +4198,8 @@ def get_cashier_bet_details(bet_id):
...
@@ -4222,7 +4198,8 @@ def get_cashier_bet_details(bet_id):
'fighter1_township'
:
match
.
fighter1_township
,
'fighter1_township'
:
match
.
fighter1_township
,
'fighter2_township'
:
match
.
fighter2_township
,
'fighter2_township'
:
match
.
fighter2_township
,
'venue_kampala_township'
:
match
.
venue_kampala_township
,
'venue_kampala_township'
:
match
.
venue_kampala_township
,
'status'
:
match
.
status
'status'
:
match
.
status
,
'result'
:
match
.
result
}
}
else
:
else
:
detail_data
[
'match'
]
=
None
detail_data
[
'match'
]
=
None
...
@@ -4508,7 +4485,8 @@ def verify_bet_details(bet_id):
...
@@ -4508,7 +4485,8 @@ def verify_bet_details(bet_id):
'fighter1_township'
:
match
.
fighter1_township
,
'fighter1_township'
:
match
.
fighter1_township
,
'fighter2_township'
:
match
.
fighter2_township
,
'fighter2_township'
:
match
.
fighter2_township
,
'venue_kampala_township'
:
match
.
venue_kampala_township
,
'venue_kampala_township'
:
match
.
venue_kampala_township
,
'status'
:
match
.
status
'status'
:
match
.
status
,
'result'
:
match
.
result
}
}
else
:
else
:
detail_data
[
'match'
]
=
None
detail_data
[
'match'
]
=
None
...
...
mbetterclient/web_dashboard/templates/dashboard/cashier_verify_bet.html
View file @
e806a118
...
@@ -590,6 +590,7 @@ function displayBetDetails(bet) {
...
@@ -590,6 +590,7 @@ function displayBetDetails(bet) {
<tr>
<tr>
<td><strong>Match #
${
detail
.
match
?
detail
.
match
.
match_number
:
'Unknown'
}
</strong><br>
<td><strong>Match #
${
detail
.
match
?
detail
.
match
.
match_number
:
'Unknown'
}
</strong><br>
<small class="text-muted">
${
detail
.
match
?
detail
.
match
.
fighter1_township
+
' vs '
+
detail
.
match
.
fighter2_township
:
'Match info unavailable'
}
</small>
<small class="text-muted">
${
detail
.
match
?
detail
.
match
.
fighter1_township
+
' vs '
+
detail
.
match
.
fighter2_township
:
'Match info unavailable'
}
</small>
${
detail
.
match
&&
detail
.
match
.
result
?
`<br><small class="text-info"><i class="fas fa-trophy me-1"></i>Result:
${
detail
.
match
.
result
}
</small>`
:
''
}
</td>
</td>
<td><span class="badge bg-primary">
${
detail
.
outcome
}
</span></td>
<td><span class="badge bg-primary">
${
detail
.
outcome
}
</span></td>
<td><strong class="currency-amount" data-amount="
${
detail
.
amount
}
">
${
formatCurrency
(
detail
.
amount
)}
</strong></td>
<td><strong class="currency-amount" data-amount="
${
detail
.
amount
}
">
${
formatCurrency
(
detail
.
amount
)}
</strong></td>
...
...
mbetterclient/web_dashboard/templates/dashboard/verify_bet.html
View file @
e806a118
...
@@ -590,6 +590,7 @@ function displayBetDetails(bet) {
...
@@ -590,6 +590,7 @@ function displayBetDetails(bet) {
<tr>
<tr>
<td><strong>Match #
${
detail
.
match
?
detail
.
match
.
match_number
:
'Unknown'
}
</strong><br>
<td><strong>Match #
${
detail
.
match
?
detail
.
match
.
match_number
:
'Unknown'
}
</strong><br>
<small class="text-muted">
${
detail
.
match
?
detail
.
match
.
fighter1_township
+
' vs '
+
detail
.
match
.
fighter2_township
:
'Match info unavailable'
}
</small>
<small class="text-muted">
${
detail
.
match
?
detail
.
match
.
fighter1_township
+
' vs '
+
detail
.
match
.
fighter2_township
:
'Match info unavailable'
}
</small>
${
detail
.
match
&&
detail
.
match
.
result
?
`<br><small class="text-info"><i class="fas fa-trophy me-1"></i>Result:
${
detail
.
match
.
result
}
</small>`
:
''
}
</td>
</td>
<td><span class="badge bg-primary">
${
detail
.
outcome
}
</span></td>
<td><span class="badge bg-primary">
${
detail
.
outcome
}
</span></td>
<td><strong class="currency-amount" data-amount="
${
detail
.
amount
}
">
${
formatCurrency
(
detail
.
amount
)}
</strong></td>
<td><strong class="currency-amount" data-amount="
${
detail
.
amount
}
">
${
formatCurrency
(
detail
.
amount
)}
</strong></td>
...
...
mbetterclient/web_dashboard/templates/dashboard/verify_bet_mobile.html
View file @
e806a118
...
@@ -579,6 +579,7 @@
...
@@ -579,6 +579,7 @@
<div class="col-8">
<div class="col-8">
<h6 class="fw-bold mb-1">Match #
${
detail
.
match
?
detail
.
match
.
match_number
:
'Unknown'
}
</h6>
<h6 class="fw-bold mb-1">Match #
${
detail
.
match
?
detail
.
match
.
match_number
:
'Unknown'
}
</h6>
<p class="text-muted small mb-1">
${
detail
.
match
?
detail
.
match
.
fighter1_township
+
' vs '
+
detail
.
match
.
fighter2_township
:
'Match info unavailable'
}
</p>
<p class="text-muted small mb-1">
${
detail
.
match
?
detail
.
match
.
fighter1_township
+
' vs '
+
detail
.
match
.
fighter2_township
:
'Match info unavailable'
}
</p>
${
detail
.
match
&&
detail
.
match
.
result
?
`<p class="text-info small mb-1"><i class="fas fa-trophy me-1"></i>Result:
${
detail
.
match
.
result
}
</p>`
:
''
}
<span class="badge bg-primary">
${
detail
.
outcome
}
</span>
<span class="badge bg-primary">
${
detail
.
outcome
}
</span>
</div>
</div>
<div class="col-4 text-end">
<div class="col-4 text-end">
...
...
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