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
7b742d32
Commit
7b742d32
authored
Nov 22, 2025
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Intro stage complete, fixture update management for failed downloads
completed
parent
816816c7
Changes
10
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
937 additions
and
495 deletions
+937
-495
client.py
mbetterclient/api_client/client.py
+361
-50
application.py
mbetterclient/core/application.py
+66
-1
games_thread.py
mbetterclient/core/games_thread.py
+217
-34
app.py
mbetterclient/web_dashboard/app.py
+99
-16
routes.py
mbetterclient/web_dashboard/routes.py
+40
-69
dashboard.js
mbetterclient/web_dashboard/static/js/dashboard.js
+154
-7
base.html
mbetterclient/web_dashboard/templates/base.html
+0
-312
bets.html
mbetterclient/web_dashboard/templates/dashboard/bets.html
+0
-2
cashier.html
mbetterclient/web_dashboard/templates/dashboard/cashier.html
+0
-2
new_bet.html
mbetterclient/web_dashboard/templates/dashboard/new_bet.html
+0
-2
No files found.
mbetterclient/api_client/client.py
View file @
7b742d32
This diff is collapsed.
Click to expand it.
mbetterclient/core/application.py
View file @
7b742d32
...
@@ -52,6 +52,7 @@ class MbetterClientApplication:
...
@@ -52,6 +52,7 @@ class MbetterClientApplication:
# Timer for automated game start
# Timer for automated game start
self
.
_game_start_timer
:
Optional
[
threading
.
Timer
]
=
None
self
.
_game_start_timer
:
Optional
[
threading
.
Timer
]
=
None
self
.
_original_timer_interval
:
Optional
[
int
]
=
None
# Store original interval for retries
logger
.
info
(
"MbetterClient application initialized"
)
logger
.
info
(
"MbetterClient application initialized"
)
...
@@ -178,6 +179,7 @@ class MbetterClientApplication:
...
@@ -178,6 +179,7 @@ class MbetterClientApplication:
self
.
message_bus
.
subscribe
(
"core"
,
MessageType
.
SYSTEM_SHUTDOWN
,
self
.
_handle_shutdown_message
)
self
.
message_bus
.
subscribe
(
"core"
,
MessageType
.
SYSTEM_SHUTDOWN
,
self
.
_handle_shutdown_message
)
self
.
message_bus
.
subscribe
(
"core"
,
MessageType
.
CONFIG_UPDATE
,
self
.
_handle_config_update
)
self
.
message_bus
.
subscribe
(
"core"
,
MessageType
.
CONFIG_UPDATE
,
self
.
_handle_config_update
)
self
.
message_bus
.
subscribe
(
"core"
,
MessageType
.
LOG_ENTRY
,
self
.
_handle_log_entry
)
self
.
message_bus
.
subscribe
(
"core"
,
MessageType
.
LOG_ENTRY
,
self
.
_handle_log_entry
)
self
.
message_bus
.
subscribe
(
"core"
,
MessageType
.
GAME_STATUS
,
self
.
_handle_game_status_response
)
logger
.
info
(
"Message bus initialized"
)
logger
.
info
(
"Message bus initialized"
)
return
True
return
True
...
@@ -611,6 +613,8 @@ class MbetterClientApplication:
...
@@ -611,6 +613,8 @@ class MbetterClientApplication:
self
.
_handle_config_request
(
message
)
self
.
_handle_config_request
(
message
)
elif
message
.
type
==
MessageType
.
START_GAME
:
elif
message
.
type
==
MessageType
.
START_GAME
:
self
.
_handle_start_game_message
(
message
)
self
.
_handle_start_game_message
(
message
)
elif
message
.
type
==
MessageType
.
GAME_STATUS
:
self
.
_handle_game_status_response
(
message
)
elif
message
.
type
==
MessageType
.
SYSTEM_SHUTDOWN
:
elif
message
.
type
==
MessageType
.
SYSTEM_SHUTDOWN
:
self
.
_handle_shutdown_message
(
message
)
self
.
_handle_shutdown_message
(
message
)
else
:
else
:
...
@@ -758,6 +762,41 @@ class MbetterClientApplication:
...
@@ -758,6 +762,41 @@ class MbetterClientApplication:
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to handle START_GAME message: {e}"
)
logger
.
error
(
f
"Failed to handle START_GAME message: {e}"
)
def
_handle_game_status_response
(
self
,
message
:
Message
):
"""Handle GAME_STATUS responses, particularly for timer-initiated START_GAME failures"""
try
:
status
=
message
.
data
.
get
(
"status"
,
"unknown"
)
sender
=
message
.
sender
# Only process responses that might be related to our timer-initiated START_GAME
# We check if we have an active timer that might need restarting
if
self
.
_game_start_timer
is
None
and
self
.
_original_timer_interval
is
None
:
# No active timer management needed
return
# Check if this is a failure response that should trigger timer restart
failure_statuses
=
[
"waiting_for_downloads"
,
"discarded"
,
"error"
,
"no_matches"
]
if
status
in
failure_statuses
:
logger
.
info
(
f
"START_GAME failed with status '{status}' from {sender} - restarting timer"
)
# Cancel any existing timer
self
.
_cancel_game_timer
()
# Restart timer with original interval
if
self
.
_original_timer_interval
is
not
None
:
logger
.
info
(
f
"Restarting game start timer with original interval: {self._original_timer_interval} minutes"
)
self
.
_start_game_timer_with_interval
(
self
.
_original_timer_interval
)
else
:
logger
.
warning
(
"No original timer interval available for restart"
)
elif
status
==
"started"
:
logger
.
info
(
f
"START_GAME succeeded with status '{status}' from {sender} - timer job completed"
)
# Game started successfully, clear timer state
self
.
_original_timer_interval
=
None
except
Exception
as
e
:
logger
.
error
(
f
"Failed to handle GAME_STATUS response: {e}"
)
def
_run_additional_tasks
(
self
):
def
_run_additional_tasks
(
self
):
"""Placeholder for additional periodic tasks"""
"""Placeholder for additional periodic tasks"""
...
@@ -770,6 +809,9 @@ class MbetterClientApplication:
...
@@ -770,6 +809,9 @@ class MbetterClientApplication:
if
self
.
_start_timer_minutes
is
None
:
if
self
.
_start_timer_minutes
is
None
:
return
return
# Store the original interval for potential retries
self
.
_original_timer_interval
=
self
.
_start_timer_minutes
# Special case: --start-timer 0 means 10 seconds delay for system initialization
# Special case: --start-timer 0 means 10 seconds delay for system initialization
if
self
.
_start_timer_minutes
==
0
:
if
self
.
_start_timer_minutes
==
0
:
delay_seconds
=
10
delay_seconds
=
10
...
@@ -782,6 +824,27 @@ class MbetterClientApplication:
...
@@ -782,6 +824,27 @@ class MbetterClientApplication:
self
.
_game_start_timer
.
daemon
=
True
self
.
_game_start_timer
.
daemon
=
True
self
.
_game_start_timer
.
start
()
self
.
_game_start_timer
.
start
()
def
_start_game_timer_with_interval
(
self
,
minutes
:
int
):
"""Start the game timer with a specific interval (used for retries)"""
if
minutes
<
0
:
logger
.
error
(
f
"Invalid timer interval: {minutes} minutes"
)
return
# Update stored interval
self
.
_original_timer_interval
=
minutes
# Special case: timer 0 means 10 seconds delay for system initialization
if
minutes
==
0
:
delay_seconds
=
10
logger
.
info
(
f
"Restarting command line game timer: 0 minutes = 10 seconds delay for system initialization"
)
else
:
delay_seconds
=
minutes
*
60
logger
.
info
(
f
"Restarting command line game timer: {minutes} minutes ({delay_seconds} seconds)"
)
self
.
_game_start_timer
=
threading
.
Timer
(
delay_seconds
,
self
.
_on_game_timer_expired
)
self
.
_game_start_timer
.
daemon
=
True
self
.
_game_start_timer
.
start
()
def
_on_game_timer_expired
(
self
):
def
_on_game_timer_expired
(
self
):
"""Called when the game start timer expires"""
"""Called when the game start timer expires"""
logger
.
info
(
"Game start timer expired, sending START_GAME message"
)
logger
.
info
(
"Game start timer expired, sending START_GAME message"
)
...
@@ -804,6 +867,7 @@ class MbetterClientApplication:
...
@@ -804,6 +867,7 @@ class MbetterClientApplication:
logger
.
info
(
"Cancelling game start timer"
)
logger
.
info
(
"Cancelling game start timer"
)
self
.
_game_start_timer
.
cancel
()
self
.
_game_start_timer
.
cancel
()
self
.
_game_start_timer
=
None
self
.
_game_start_timer
=
None
# Note: We keep _original_timer_interval for potential retries
def
_check_component_health
(
self
):
def
_check_component_health
(
self
):
"""Check health of all components"""
"""Check health of all components"""
...
@@ -841,8 +905,9 @@ class MbetterClientApplication:
...
@@ -841,8 +905,9 @@ class MbetterClientApplication:
self
.
running
=
False
self
.
running
=
False
self
.
shutdown_event
.
set
()
self
.
shutdown_event
.
set
()
# Cancel game timer if running
# Cancel game timer if running
and clear timer state
self
.
_cancel_game_timer
()
self
.
_cancel_game_timer
()
self
.
_original_timer_interval
=
None
# Send shutdown message to all components
# Send shutdown message to all components
if
self
.
message_bus
:
if
self
.
message_bus
:
...
...
mbetterclient/core/games_thread.py
View file @
7b742d32
This diff is collapsed.
Click to expand it.
mbetterclient/web_dashboard/app.py
View file @
7b742d32
...
@@ -5,7 +5,7 @@ Flask web dashboard application for MbetterClient
...
@@ -5,7 +5,7 @@ Flask web dashboard application for MbetterClient
import
time
import
time
import
logging
import
logging
from
pathlib
import
Path
from
pathlib
import
Path
from
typing
import
Optional
,
Dict
,
Any
from
typing
import
Optional
,
Dict
,
Any
,
List
from
flask
import
Flask
,
request
,
jsonify
,
render_template
,
redirect
,
url_for
,
session
,
g
from
flask
import
Flask
,
request
,
jsonify
,
render_template
,
redirect
,
url_for
,
session
,
g
from
flask_login
import
LoginManager
,
login_required
,
current_user
from
flask_login
import
LoginManager
,
login_required
,
current_user
from
flask_jwt_extended
import
JWTManager
,
create_access_token
,
jwt_required
as
flask_jwt_required
from
flask_jwt_extended
import
JWTManager
,
create_access_token
,
jwt_required
as
flask_jwt_required
...
@@ -57,6 +57,12 @@ class WebDashboard(ThreadedComponent):
...
@@ -57,6 +57,12 @@ class WebDashboard(ThreadedComponent):
"match_id"
:
None
,
"match_id"
:
None
,
"start_time"
:
None
"start_time"
:
None
}
}
# Client notification queue for long-polling clients
self
.
notification_queue
:
List
[
Dict
[
str
,
Any
]]
=
[]
self
.
notification_lock
=
threading
.
Lock
()
self
.
waiting_clients
:
List
[
threading
.
Event
]
=
[]
# Events for waiting long-poll clients
self
.
waiting_clients_lock
=
threading
.
Lock
()
# 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
)
...
@@ -89,6 +95,10 @@ class WebDashboard(ThreadedComponent):
...
@@ -89,6 +95,10 @@ class WebDashboard(ThreadedComponent):
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
CONFIG_UPDATE
,
self
.
_handle_config_update
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
CONFIG_UPDATE
,
self
.
_handle_config_update
)
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
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
CUSTOM
,
self
.
_handle_custom_message
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
CUSTOM
,
self
.
_handle_custom_message
)
# Subscribe to messages for client notifications
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
START_GAME
,
self
.
_handle_client_notification
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
MATCH_START
,
self
.
_handle_client_notification
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
GAME_STATUS
,
self
.
_handle_client_notification
)
logger
.
info
(
"WebDashboard initialized successfully"
)
logger
.
info
(
"WebDashboard initialized successfully"
)
return
True
return
True
...
@@ -537,6 +547,7 @@ class WebDashboard(ThreadedComponent):
...
@@ -537,6 +547,7 @@ class WebDashboard(ThreadedComponent):
try
:
try
:
response
=
message
.
data
.
get
(
"response"
)
response
=
message
.
data
.
get
(
"response"
)
timer_update
=
message
.
data
.
get
(
"timer_update"
)
timer_update
=
message
.
data
.
get
(
"timer_update"
)
fixture_status_update
=
message
.
data
.
get
(
"fixture_status_update"
)
if
response
==
"timer_state"
:
if
response
==
"timer_state"
:
# Update stored timer state
# Update stored timer state
...
@@ -560,24 +571,96 @@ class WebDashboard(ThreadedComponent):
...
@@ -560,24 +571,96 @@ class WebDashboard(ThreadedComponent):
# Handle periodic timer updates from match_timer component
# Handle periodic timer updates from match_timer component
self
.
current_timer_state
.
update
(
timer_update
)
self
.
current_timer_state
.
update
(
timer_update
)
logger
.
debug
(
f
"Timer update received: {timer_update}"
)
logger
.
debug
(
f
"Timer update received: {timer_update}"
)
# Broadcast timer update to connected clients via global message bus
# Add timer update to notification queue for long-polling clients
try
:
self
.
_add_client_notification
(
"TIMER_UPDATE"
,
timer_update
,
message
.
timestamp
)
timer_update_message
=
Message
(
type
=
MessageType
.
CUSTOM
,
elif
fixture_status_update
:
sender
=
self
.
name
,
# Handle fixture status updates from games_thread
data
=
{
logger
.
debug
(
f
"Fixture status update received: {fixture_status_update}"
)
"timer_update"
:
timer_update
,
"timestamp"
:
time
.
time
()
# Add fixture status update to notification queue for long-polling clients
}
self
.
_add_client_notification
(
"FIXTURE_STATUS_UPDATE"
,
fixture_status_update
,
message
.
timestamp
)
)
self
.
message_bus
.
publish
(
timer_update_message
,
broadcast
=
True
)
logger
.
debug
(
"Timer update broadcasted to clients"
)
except
Exception
as
broadcast_e
:
logger
.
error
(
f
"Failed to broadcast timer update: {broadcast_e}"
)
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to handle custom message: {e}"
)
logger
.
error
(
f
"Failed to handle custom message: {e}"
)
def
_handle_client_notification
(
self
,
message
:
Message
):
"""Handle messages that should be sent to long-polling clients"""
try
:
# Convert message to notification format
notification_data
=
{
"type"
:
message
.
type
.
value
,
"data"
:
message
.
data
,
"timestamp"
:
message
.
timestamp
,
"sender"
:
message
.
sender
}
# Add to notification queue
self
.
_add_client_notification
(
message
.
type
.
value
,
message
.
data
,
message
.
timestamp
,
message
.
sender
)
except
Exception
as
e
:
logger
.
error
(
f
"Failed to handle client notification: {e}"
)
def
_add_client_notification
(
self
,
notification_type
:
str
,
data
:
Dict
[
str
,
Any
],
timestamp
:
float
,
sender
:
str
=
None
):
"""Add notification to the client queue"""
try
:
with
self
.
notification_lock
:
notification
=
{
"type"
:
notification_type
,
"data"
:
data
,
"timestamp"
:
timestamp
}
if
sender
:
notification
[
"sender"
]
=
sender
self
.
notification_queue
.
append
(
notification
)
# Keep queue size reasonable (limit to last 100 notifications)
if
len
(
self
.
notification_queue
)
>
100
:
self
.
notification_queue
=
self
.
notification_queue
[
-
50
:]
logger
.
debug
(
f
"Added client notification: {notification_type}"
)
# Wake up all waiting clients
with
self
.
waiting_clients_lock
:
for
event
in
self
.
waiting_clients
:
event
.
set
()
self
.
waiting_clients
.
clear
()
except
Exception
as
e
:
logger
.
error
(
f
"Failed to add client notification: {e}"
)
def
get_pending_notifications
(
self
)
->
List
[
Dict
[
str
,
Any
]]:
"""Get the first pending notification for a client (one at a time)"""
try
:
with
self
.
notification_lock
:
if
self
.
notification_queue
:
# Return only the first notification and remove it from queue
notification
=
self
.
notification_queue
.
pop
(
0
)
return
[
notification
]
return
[]
except
Exception
as
e
:
logger
.
error
(
f
"Failed to get pending notifications: {e}"
)
return
[]
def
register_waiting_client
(
self
,
event
:
threading
.
Event
):
"""Register a waiting client Event to be notified when notifications arrive"""
try
:
with
self
.
waiting_clients_lock
:
self
.
waiting_clients
.
append
(
event
)
except
Exception
as
e
:
logger
.
error
(
f
"Failed to register waiting client: {e}"
)
def
unregister_waiting_client
(
self
,
event
:
threading
.
Event
):
"""Unregister a waiting client Event"""
try
:
with
self
.
waiting_clients_lock
:
if
event
in
self
.
waiting_clients
:
self
.
waiting_clients
.
remove
(
event
)
except
Exception
as
e
:
logger
.
error
(
f
"Failed to unregister waiting client: {e}"
)
def
get_app_context
(
self
):
def
get_app_context
(
self
):
"""Get Flask application context"""
"""Get Flask application context"""
...
...
mbetterclient/web_dashboard/routes.py
View file @
7b742d32
...
@@ -2600,90 +2600,61 @@ def notifications():
...
@@ -2600,90 +2600,61 @@ def notifications():
import
threading
import
threading
import
ssl
import
ssl
import
socket
import
socket
from
..core.message_bus
import
MessageType
# Get timeout from query parameter (default 30 seconds)
# Get timeout from query parameter (default 30 seconds)
timeout
=
int
(
request
.
args
.
get
(
'timeout'
,
30
))
timeout
=
int
(
request
.
args
.
get
(
'timeout'
,
30
))
if
timeout
>
60
:
# Max 60 seconds
if
timeout
>
60
:
# Max 60 seconds
timeout
=
60
timeout
=
60
# Create a queue for this client
# Create an event for timeout handling
notification_queue
=
[]
notification_received
=
threading
.
Event
()
notification_received
=
threading
.
Event
()
def
message_handler
(
message
):
# Check for pending notifications from the web dashboard
"""Handle incoming messages for this client"""
if
hasattr
(
g
,
'main_app'
)
and
g
.
main_app
and
hasattr
(
g
.
main_app
,
'web_dashboard'
):
if
message
.
type
in
[
MessageType
.
START_GAME
,
MessageType
.
GAME_STARTED
,
MessageType
.
MATCH_START
,
MessageType
.
GAME_STATUS
]:
# Register this event with the web dashboard so it gets notified when notifications arrive
notification_data
=
{
g
.
main_app
.
web_dashboard
.
register_waiting_client
(
notification_received
)
"type"
:
message
.
type
.
value
,
"data"
:
message
.
data
,
"timestamp"
:
message
.
timestamp
,
"sender"
:
message
.
sender
}
notification_queue
.
append
(
notification_data
)
notification_received
.
set
()
elif
message
.
type
==
MessageType
.
CUSTOM
and
"timer_update"
in
message
.
data
:
# Handle timer updates from match_timer component
notification_data
=
{
"type"
:
"TIMER_UPDATE"
,
"data"
:
message
.
data
[
"timer_update"
],
"timestamp"
:
message
.
timestamp
,
"sender"
:
message
.
sender
}
notification_queue
.
append
(
notification_data
)
notification_received
.
set
()
elif
message
.
type
==
MessageType
.
CUSTOM
and
"fixture_status_update"
in
message
.
data
:
# Handle fixture status updates from games_thread
notification_data
=
{
"type"
:
"FIXTURE_STATUS_UPDATE"
,
"data"
:
message
.
data
[
"fixture_status_update"
],
"timestamp"
:
message
.
timestamp
,
"sender"
:
message
.
sender
}
notification_queue
.
append
(
notification_data
)
notification_received
.
set
()
# Subscribe to relevant message types
if
api_bp
.
message_bus
:
api_bp
.
message_bus
.
subscribe_global
(
MessageType
.
START_GAME
,
message_handler
)
api_bp
.
message_bus
.
subscribe_global
(
MessageType
.
MATCH_START
,
message_handler
)
api_bp
.
message_bus
.
subscribe_global
(
MessageType
.
GAME_STATUS
,
message_handler
)
api_bp
.
message_bus
.
subscribe_global
(
MessageType
.
CUSTOM
,
message_handler
)
# Wait for notification or timeout
notification_received
.
wait
(
timeout
=
timeout
)
# Unsubscribe from messages safely
if
api_bp
.
message_bus
:
try
:
try
:
# Use proper unsubscribe methods instead of direct removal
# First check for any pending notifications
for
msg_type
in
[
MessageType
.
START_GAME
,
MessageType
.
MATCH_START
,
MessageType
.
GAME_STATUS
,
MessageType
.
CUSTOM
]:
pending_notifications
=
g
.
main_app
.
web_dashboard
.
get_pending_notifications
()
try
:
if
pending_notifications
:
if
hasattr
(
api_bp
.
message_bus
,
'_global_handlers'
)
and
msg_type
in
api_bp
.
message_bus
.
_global_handlers
:
# Return pending notifications immediately
handlers
=
api_bp
.
message_bus
.
_global_handlers
[
msg_type
]
logger
.
debug
(
f
"Returning {len(pending_notifications)} pending notifications to client"
)
if
message_handler
in
handlers
:
response_data
=
{
handlers
.
remove
(
message_handler
)
"success"
:
True
,
except
(
AttributeError
,
KeyError
,
ValueError
)
as
e
:
"notifications"
:
pending_notifications
logger
.
debug
(
f
"Handler cleanup warning for {msg_type}: {e}"
)
}
except
Exception
as
e
:
else
:
logger
.
warning
(
f
"Error during notification cleanup: {e}"
)
# No pending notifications, wait for new ones or timeout
logger
.
debug
(
f
"Waiting for notifications with {timeout}s timeout"
)
# Prepare response data
notification_received
.
wait
(
timeout
=
timeout
)
if
notification_queue
:
# After waiting, check again for any notifications that arrived
# Return the first notification received
pending_notifications
=
g
.
main_app
.
web_dashboard
.
get_pending_notifications
()
notification
=
notification_queue
[
0
]
if
pending_notifications
:
logger
.
debug
(
f
"Notification sent to client: {notification['type']}"
)
logger
.
debug
(
f
"Returning {len(pending_notifications)} notifications after wait"
)
response_data
=
{
response_data
=
{
"success"
:
True
,
"success"
:
True
,
"notification"
:
notification
"notifications"
:
pending_notifications
}
}
else
:
# Timeout - return empty response
logger
.
debug
(
"Notification wait timed out, returning empty response"
)
response_data
=
{
"success"
:
True
,
"notifications"
:
[]
}
finally
:
# Always unregister the event when done
g
.
main_app
.
web_dashboard
.
unregister_waiting_client
(
notification_received
)
else
:
else
:
#
Timeout -
return empty response
#
Web dashboard not available,
return empty response
response_data
=
{
response_data
=
{
"success"
:
True
,
"success"
:
True
,
"notification
"
:
None
"notification
s"
:
[]
}
}
# Response data is already prepared above
# Handle SSL/connection errors gracefully when sending response
# Handle SSL/connection errors gracefully when sending response
try
:
try
:
return
jsonify
(
response_data
)
return
jsonify
(
response_data
)
...
...
mbetterclient/web_dashboard/static/js/dashboard.js
View file @
7b742d32
...
@@ -5,14 +5,20 @@
...
@@ -5,14 +5,20 @@
// Dashboard namespace
// Dashboard namespace
window
.
Dashboard
=
(
function
()
{
window
.
Dashboard
=
(
function
()
{
'use strict'
;
'use strict'
;
let
config
=
{};
let
config
=
{};
let
statusInterval
=
null
;
let
statusInterval
=
null
;
let
isOnline
=
navigator
.
onLine
;
let
isOnline
=
navigator
.
onLine
;
let
cache
=
{};
let
cache
=
{};
let
initialized
=
false
;
// Prevent multiple initializations
// Initialize dashboard
// Initialize dashboard
function
init
(
userConfig
)
{
function
init
(
userConfig
)
{
if
(
initialized
)
{
console
.
log
(
'Dashboard already initialized, skipping...'
);
return
;
}
config
=
Object
.
assign
({
config
=
Object
.
assign
({
statusUpdateInterval
:
5000
,
statusUpdateInterval
:
5000
,
apiEndpoint
:
'/api'
,
apiEndpoint
:
'/api'
,
...
@@ -36,6 +42,7 @@ window.Dashboard = (function() {
...
@@ -36,6 +42,7 @@ window.Dashboard = (function() {
// Initialize match timer
// Initialize match timer
initMatchTimer
();
initMatchTimer
();
initialized
=
true
;
console
.
log
(
'Dashboard initialized successfully'
);
console
.
log
(
'Dashboard initialized successfully'
);
}
}
...
@@ -463,9 +470,11 @@ window.Dashboard = (function() {
...
@@ -463,9 +470,11 @@ window.Dashboard = (function() {
let
lastServerSync
=
0
;
let
lastServerSync
=
0
;
let
cachedMatchInterval
=
null
;
// Cache the match interval configuration
let
cachedMatchInterval
=
null
;
// Cache the match interval configuration
const
SYNC_INTERVAL
=
30000
;
// Sync with server every 30 seconds
const
SYNC_INTERVAL
=
30000
;
// Sync with server every 30 seconds
let
longPollingActive
=
false
;
// Flag to control long polling
let
longPollingController
=
null
;
// AbortController for cancelling requests
function
initMatchTimer
()
{
function
initMatchTimer
()
{
console
.
log
(
'Initializing server-only match timer
(no local countdown)
...'
);
console
.
log
(
'Initializing server-only match timer
with real-time notifications
...'
);
// Load match interval config once at initialization
// Load match interval config once at initialization
loadMatchIntervalConfig
().
then
(
function
(
intervalSeconds
)
{
loadMatchIntervalConfig
().
then
(
function
(
intervalSeconds
)
{
...
@@ -474,9 +483,9 @@ window.Dashboard = (function() {
...
@@ -474,9 +483,9 @@ window.Dashboard = (function() {
// Initial sync with server
// Initial sync with server
syncWithServerTimer
();
syncWithServerTimer
();
//
REMOVED: No periodic sync - rely on notification
s
//
Start real-time notification polling for timer update
s
// REMOVED: No local countdown - rely on server updates only
startNotificationPolling
();
}).
catch
(
function
(
error
)
{
}).
catch
(
function
(
error
)
{
console
.
error
(
'Failed to load match timer config at initialization:'
,
error
);
console
.
error
(
'Failed to load match timer config at initialization:'
,
error
);
// Use default and continue
// Use default and continue
...
@@ -484,6 +493,9 @@ window.Dashboard = (function() {
...
@@ -484,6 +493,9 @@ window.Dashboard = (function() {
// Initial sync with server
// Initial sync with server
syncWithServerTimer
();
syncWithServerTimer
();
// Start real-time notification polling for timer updates
startNotificationPolling
();
});
});
}
}
...
@@ -568,6 +580,17 @@ window.Dashboard = (function() {
...
@@ -568,6 +580,17 @@ window.Dashboard = (function() {
}
}
}
}
function
stopNotificationPolling
()
{
console
.
log
(
'Stopping long polling for notifications...'
);
longPollingActive
=
false
;
// Cancel any ongoing request
if
(
longPollingController
)
{
longPollingController
.
abort
();
longPollingController
=
null
;
}
}
function
updateMatchTimerDisplay
()
{
function
updateMatchTimerDisplay
()
{
let
displaySeconds
=
serverTimerState
.
remaining_seconds
;
let
displaySeconds
=
serverTimerState
.
remaining_seconds
;
...
@@ -661,6 +684,130 @@ window.Dashboard = (function() {
...
@@ -661,6 +684,130 @@ window.Dashboard = (function() {
});
});
}
}
function
startNotificationPolling
()
{
console
.
log
(
'Starting long polling for real-time notifications...'
);
if
(
longPollingActive
)
{
stopNotificationPolling
();
}
longPollingActive
=
true
;
performLongPoll
();
}
function
performLongPoll
()
{
if
(
!
longPollingActive
||
!
isOnline
)
{
return
;
}
// Create AbortController for this request
longPollingController
=
new
AbortController
();
const
signal
=
longPollingController
.
signal
;
// Make long polling request (server handles 30-second timeout)
const
url
=
config
.
apiEndpoint
+
'/notifications?_='
+
Date
.
now
()
+
Math
.
random
();
const
options
=
{
method
:
'GET'
,
headers
:
{
'Content-Type'
:
'application/json'
,
},
signal
:
signal
};
fetch
(
url
,
options
)
.
then
(
function
(
response
)
{
if
(
!
response
.
ok
)
{
throw
new
Error
(
'HTTP '
+
response
.
status
);
}
return
response
.
json
();
})
.
then
(
function
(
data
)
{
if
(
data
.
success
&&
data
.
notifications
&&
data
.
notifications
.
length
>
0
)
{
// Process each notification
data
.
notifications
.
forEach
(
function
(
notification
)
{
handleNotification
(
notification
);
});
}
// Immediately start the next long poll after processing notifications
if
(
longPollingActive
)
{
// Small delay to prevent overwhelming the server
setTimeout
(
performLongPoll
,
100
);
}
})
.
catch
(
function
(
error
)
{
if
(
error
.
name
===
'AbortError'
)
{
// Request was cancelled, don't restart
console
.
debug
(
'Long poll cancelled'
);
return
;
}
console
.
debug
(
'Long poll failed:'
,
error
.
message
);
// Retry after a delay if still active
if
(
longPollingActive
)
{
setTimeout
(
performLongPoll
,
2000
);
// Retry after 2 seconds on error
}
});
}
function
handleNotification
(
notification
)
{
console
.
log
(
'Received notification:'
,
notification
.
type
,
notification
);
if
(
notification
.
type
===
'TIMER_UPDATE'
&&
notification
.
data
)
{
// Update timer state from server notification
serverTimerState
=
{
running
:
notification
.
data
.
running
||
false
,
remaining_seconds
:
notification
.
data
.
remaining_seconds
||
0
,
total_seconds
:
notification
.
data
.
total_seconds
||
0
,
fixture_id
:
notification
.
data
.
fixture_id
||
null
,
match_id
:
notification
.
data
.
match_id
||
null
,
start_time
:
notification
.
data
.
start_time
||
null
};
// Update display immediately
updateMatchTimerDisplay
();
console
.
log
(
'Timer updated from notification:'
,
serverTimerState
);
}
else
if
(
notification
.
type
===
'START_GAME'
)
{
console
.
log
(
'Start game notification received'
);
onStartGameMessage
();
showNotification
(
'Games started - match timer is now running'
,
'success'
);
}
else
if
(
notification
.
type
===
'MATCH_START'
)
{
console
.
log
(
'Match started:'
,
notification
.
data
);
showNotification
(
'New match has started!'
,
'info'
);
}
else
if
(
notification
.
type
===
'GAME_STATUS'
)
{
console
.
log
(
'Game status update:'
,
notification
.
data
);
// Update system status if provided
if
(
notification
.
data
.
status
)
{
const
systemStatus
=
document
.
getElementById
(
'system-status'
);
if
(
systemStatus
)
{
systemStatus
.
textContent
=
notification
.
data
.
status
.
charAt
(
0
).
toUpperCase
()
+
notification
.
data
.
status
.
slice
(
1
);
systemStatus
.
className
=
'badge '
+
(
notification
.
data
.
status
===
'running'
?
'bg-success'
:
'bg-secondary'
);
}
}
}
else
if
(
notification
.
type
===
'FIXTURE_STATUS_UPDATE'
)
{
console
.
log
(
'Fixture status update:'
,
notification
.
data
);
// Trigger page refresh if on fixtures page
if
(
window
.
location
.
pathname
.
includes
(
'/fixtures'
))
{
// Refresh fixtures data if function exists
if
(
typeof
loadFixtures
===
'function'
)
{
loadFixtures
();
}
if
(
typeof
loadFixtureDetails
===
'function'
)
{
loadFixtureDetails
();
}
}
}
else
{
console
.
log
(
'Unknown notification type:'
,
notification
.
type
);
}
}
function
onStartGameMessage
()
{
function
onStartGameMessage
()
{
console
.
log
(
'Received START_GAME message, initializing timer...'
);
console
.
log
(
'Received START_GAME message, initializing timer...'
);
...
@@ -675,7 +822,6 @@ window.Dashboard = (function() {
...
@@ -675,7 +822,6 @@ window.Dashboard = (function() {
// Update display
// Update display
updateMatchTimerDisplay
();
updateMatchTimerDisplay
();
startLocalCountdown
();
showNotification
(
'Games started - match timer is now running'
,
'success'
);
showNotification
(
'Games started - match timer is now running'
,
'success'
);
});
});
...
@@ -731,7 +877,8 @@ window.Dashboard = (function() {
...
@@ -731,7 +877,8 @@ window.Dashboard = (function() {
startMatchTimer
:
startMatchTimer
,
startMatchTimer
:
startMatchTimer
,
stopMatchTimer
:
stopMatchTimer
,
stopMatchTimer
:
stopMatchTimer
,
resetMatchTimer
:
resetMatchTimer
,
resetMatchTimer
:
resetMatchTimer
,
startNextMatch
:
startNextMatch
startNextMatch
:
startNextMatch
,
stopNotificationPolling
:
stopNotificationPolling
};
};
})();
})();
...
...
mbetterclient/web_dashboard/templates/base.html
View file @
7b742d32
This diff is collapsed.
Click to expand it.
mbetterclient/web_dashboard/templates/dashboard/bets.html
View file @
7b742d32
...
@@ -1082,8 +1082,6 @@ function showNotification(message, type = 'info') {
...
@@ -1082,8 +1082,6 @@ function showNotification(message, type = 'info') {
},
3000
);
},
3000
);
}
}
</script>
</script>
<!-- Include the main dashboard.js for timer functionality -->
<script
src=
"{{ url_for('static', filename='js/dashboard.js') }}"
></script>
<script>
<script>
// Initialize dashboard with timer functionality
// Initialize dashboard with timer functionality
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
...
...
mbetterclient/web_dashboard/templates/dashboard/cashier.html
View file @
7b742d32
...
@@ -738,8 +738,6 @@ function loadAvailableTemplates() {
...
@@ -738,8 +738,6 @@ function loadAvailableTemplates() {
});
});
}
}
</script>
</script>
<!-- Include the main dashboard.js for timer functionality -->
<script
src=
"{{ url_for('static', filename='js/dashboard.js') }}"
></script>
<script>
<script>
// Initialize dashboard with timer functionality
// Initialize dashboard with timer functionality
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
...
...
mbetterclient/web_dashboard/templates/dashboard/new_bet.html
View file @
7b742d32
...
@@ -927,8 +927,6 @@ function showNotification(message, type = 'info') {
...
@@ -927,8 +927,6 @@ function showNotification(message, type = 'info') {
},
3000
);
},
3000
);
}
}
</script>
</script>
<!-- Include the main dashboard.js for timer functionality -->
<script
src=
"{{ url_for('static', filename='js/dashboard.js') }}"
></script>
<script>
<script>
// Initialize dashboard with timer functionality
// Initialize dashboard with timer functionality
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
...
...
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