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
e1efad39
Commit
e1efad39
authored
Nov 21, 2025
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Match creations and cleanup of old matches now working correctly
parent
4e06f4e7
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
1116 additions
and
306 deletions
+1116
-306
games_thread.py
mbetterclient/core/games_thread.py
+69
-17
match_timer.py
mbetterclient/core/match_timer.py
+9
-10
message_bus.py
mbetterclient/core/message_bus.py
+12
-0
migrations.py
mbetterclient/database/migrations.py
+177
-0
models.py
mbetterclient/database/models.py
+2
-2
player.py
mbetterclient/qt_player/player.py
+73
-8
fixtures.html
mbetterclient/qt_player/templates/fixtures.html
+40
-56
match.html
mbetterclient/qt_player/templates/match.html
+733
-212
routes.py
mbetterclient/web_dashboard/routes.py
+1
-1
No files found.
mbetterclient/core/games_thread.py
View file @
e1efad39
...
@@ -28,14 +28,14 @@ class GamesThread(ThreadedComponent):
...
@@ -28,14 +28,14 @@ class GamesThread(ThreadedComponent):
self
.
message_queue
=
None
self
.
message_queue
=
None
def
_cleanup_stale_ingame_matches
(
self
):
def
_cleanup_stale_ingame_matches
(
self
):
"""Clean up any stale 'ingame' matches from previous crashed sessions"""
"""Clean up any stale 'ingame' matches from previous crashed sessions
and old 'bet' fixtures
"""
try
:
try
:
session
=
self
.
db_manager
.
get_session
()
session
=
self
.
db_manager
.
get_session
()
try
:
try
:
# Get today's date
# Get today's date
today
=
datetime
.
now
()
.
date
()
today
=
datetime
.
now
()
.
date
()
#
Find all ingame matches from today that might be stale
#
PART 1: Clean up stale 'ingame' matches from today (existing logic)
stale_matches
=
session
.
query
(
MatchModel
)
.
filter
(
stale_matches
=
session
.
query
(
MatchModel
)
.
filter
(
MatchModel
.
start_time
.
isnot
(
None
),
MatchModel
.
start_time
.
isnot
(
None
),
MatchModel
.
start_time
>=
datetime
.
combine
(
today
,
datetime
.
min
.
time
()),
MatchModel
.
start_time
>=
datetime
.
combine
(
today
,
datetime
.
min
.
time
()),
...
@@ -44,26 +44,63 @@ class GamesThread(ThreadedComponent):
...
@@ -44,26 +44,63 @@ class GamesThread(ThreadedComponent):
MatchModel
.
active_status
==
True
MatchModel
.
active_status
==
True
)
.
all
()
)
.
all
()
if
not
stale_matches
:
if
stale_matches
:
logger
.
info
(
"No stale ingame matches found"
)
return
logger
.
info
(
f
"Found {len(stale_matches)} stale ingame matches - cleaning up"
)
logger
.
info
(
f
"Found {len(stale_matches)} stale ingame matches - cleaning up"
)
# Change status to pending and set active_status to False
for
match
in
stale_matches
:
for
match
in
stale_matches
:
logger
.
info
(
f
"Cleaning up stale match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township}"
)
logger
.
info
(
f
"Cleaning up stale match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township}"
)
match
.
status
=
'pending'
match
.
status
=
'pending'
match
.
active_status
=
False
match
.
active_status
=
False
session
.
commit
()
session
.
commit
()
logger
.
info
(
f
"Cleaned up {len(stale_matches)} stale ingame matches"
)
logger
.
info
(
f
"Cleaned up {len(stale_matches)} stale ingame matches"
)
else
:
logger
.
info
(
"No stale ingame matches found"
)
# PART 2: Clean up ALL old 'bet' fixtures (new logic)
old_bet_matches
=
session
.
query
(
MatchModel
)
.
filter
(
MatchModel
.
status
==
'bet'
,
MatchModel
.
active_status
==
True
,
# Exclude today's matches to avoid interfering with active games
~
MatchModel
.
start_time
.
between
(
datetime
.
combine
(
today
,
datetime
.
min
.
time
()),
datetime
.
combine
(
today
,
datetime
.
max
.
time
())
)
)
.
all
()
if
old_bet_matches
:
logger
.
info
(
f
"Found {len(old_bet_matches)} old 'bet' matches - cancelling them"
)
for
match
in
old_bet_matches
:
logger
.
info
(
f
"Cancelling old bet match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township}"
)
match
.
status
=
'cancelled'
# Cancel/refund associated bets
self
.
_cancel_match_bets
(
match
.
id
,
session
)
session
.
commit
()
logger
.
info
(
f
"Cancelled {len(old_bet_matches)} old bet matches"
)
else
:
logger
.
info
(
"No old bet matches found to cancel"
)
finally
:
finally
:
session
.
close
()
session
.
close
()
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to cleanup stale ingame matches: {e}"
)
logger
.
error
(
f
"Failed to cleanup stale matches: {e}"
)
def
_cancel_match_bets
(
self
,
match_id
:
int
,
session
):
"""Cancel all pending bets for a match"""
try
:
# Update all pending bets for this match to 'cancelled'
cancelled_count
=
session
.
query
(
BetDetailModel
)
.
filter
(
BetDetailModel
.
match_id
==
match_id
,
BetDetailModel
.
result
==
'pending'
)
.
update
({
'result'
:
'cancelled'
})
if
cancelled_count
>
0
:
logger
.
info
(
f
"Cancelled {cancelled_count} pending bets for match {match_id}"
)
except
Exception
as
e
:
logger
.
error
(
f
"Failed to cancel bets for match {match_id}: {e}"
)
def
initialize
(
self
)
->
bool
:
def
initialize
(
self
)
->
bool
:
"""Initialize the games thread"""
"""Initialize the games thread"""
...
@@ -836,6 +873,14 @@ class GamesThread(ThreadedComponent):
...
@@ -836,6 +873,14 @@ class GamesThread(ThreadedComponent):
logger
.
info
(
f
"🎬 Dispatching START_INTRO message for fixture {fixture_id}"
)
logger
.
info
(
f
"🎬 Dispatching START_INTRO message for fixture {fixture_id}"
)
self
.
_dispatch_start_intro
(
fixture_id
)
self
.
_dispatch_start_intro
(
fixture_id
)
# Broadcast GAME_STARTED message to notify all components that game has started with this fixture
game_started_message
=
MessageBuilder
.
game_started
(
sender
=
self
.
name
,
fixture_id
=
fixture_id
)
self
.
message_bus
.
publish
(
game_started_message
,
broadcast
=
True
)
logger
.
info
(
f
"🎯 Broadcast GAME_STARTED message for fixture {fixture_id}"
)
# Refresh dashboard statuses
# Refresh dashboard statuses
self
.
_refresh_dashboard_statuses
()
self
.
_refresh_dashboard_statuses
()
...
@@ -1881,11 +1926,11 @@ class GamesThread(ThreadedComponent):
...
@@ -1881,11 +1926,11 @@ class GamesThread(ThreadedComponent):
logger
.
error
(
f
"Failed to send NEXT_MATCH: {e}"
)
logger
.
error
(
f
"Failed to send NEXT_MATCH: {e}"
)
def
_select_random_completed_matches
(
self
,
count
:
int
,
session
)
->
List
[
MatchModel
]:
def
_select_random_completed_matches
(
self
,
count
:
int
,
session
)
->
List
[
MatchModel
]:
"""Select random completed matches from the database"""
"""Select random completed matches from the database
(including cancelled and failed)
"""
try
:
try
:
# Get all completed matches (status = 'done')
# Get all completed matches (status = 'done'
, 'cancelled', or 'failed'
)
completed_matches
=
session
.
query
(
MatchModel
)
.
filter
(
completed_matches
=
session
.
query
(
MatchModel
)
.
filter
(
MatchModel
.
status
==
'done'
,
MatchModel
.
status
.
in_
([
'done'
,
'cancelled'
,
'failed'
])
,
MatchModel
.
active_status
==
True
MatchModel
.
active_status
==
True
)
.
all
()
)
.
all
()
...
@@ -1907,7 +1952,13 @@ class GamesThread(ThreadedComponent):
...
@@ -1907,7 +1952,13 @@ class GamesThread(ThreadedComponent):
"""Create new matches in the fixture by copying from old completed matches"""
"""Create new matches in the fixture by copying from old completed matches"""
try
:
try
:
now
=
datetime
.
utcnow
()
now
=
datetime
.
utcnow
()
match_number
=
1
# Find the maximum match_number in the fixture and increment from there
max_match_number
=
session
.
query
(
MatchModel
.
match_number
)
.
filter
(
MatchModel
.
fixture_id
==
fixture_id
)
.
order_by
(
MatchModel
.
match_number
.
desc
())
.
first
()
match_number
=
(
max_match_number
[
0
]
+
1
)
if
max_match_number
else
1
for
old_match
in
old_matches
:
for
old_match
in
old_matches
:
# Create a new match based on the old one
# Create a new match based on the old one
...
@@ -1959,6 +2010,7 @@ class GamesThread(ThreadedComponent):
...
@@ -1959,6 +2010,7 @@ class GamesThread(ThreadedComponent):
fixture_id
=
f
"recycle_{uuid.uuid4().hex[:8]}"
fixture_id
=
f
"recycle_{uuid.uuid4().hex[:8]}"
now
=
datetime
.
utcnow
()
now
=
datetime
.
utcnow
()
# For a new fixture, start match_number from 1
match_number
=
1
match_number
=
1
for
old_match
in
old_matches
:
for
old_match
in
old_matches
:
# Create a new match based on the old one
# Create a new match based on the old one
...
...
mbetterclient/core/match_timer.py
View file @
e1efad39
...
@@ -40,7 +40,7 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -40,7 +40,7 @@ class MatchTimerComponent(ThreadedComponent):
self
.
message_bus
.
register_component
(
self
.
name
)
self
.
message_bus
.
register_component
(
self
.
name
)
# Register message handlers
# Register message handlers
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
START_GAME
,
self
.
_handle_start_game
)
self
.
message_bus
.
subscribe
(
self
.
name
,
MessageType
.
GAME_STARTED
,
self
.
_handle_game_started
)
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
)
...
@@ -106,8 +106,8 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -106,8 +106,8 @@ class MatchTimerComponent(ThreadedComponent):
logger
.
debug
(
f
"MatchTimer processing message: {message}"
)
logger
.
debug
(
f
"MatchTimer processing message: {message}"
)
# Handle messages directly since some messages don't trigger subscription handlers
# Handle messages directly since some messages don't trigger subscription handlers
if
message
.
type
==
MessageType
.
START_GAME
:
if
message
.
type
==
MessageType
.
GAME_STARTED
:
self
.
_handle_
start_game
(
message
)
self
.
_handle_
game_started
(
message
)
elif
message
.
type
==
MessageType
.
SCHEDULE_GAMES
:
elif
message
.
type
==
MessageType
.
SCHEDULE_GAMES
:
self
.
_handle_schedule_games
(
message
)
self
.
_handle_schedule_games
(
message
)
elif
message
.
type
==
MessageType
.
CUSTOM
:
elif
message
.
type
==
MessageType
.
CUSTOM
:
...
@@ -157,12 +157,12 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -157,12 +157,12 @@ class MatchTimerComponent(ThreadedComponent):
"elapsed_seconds"
:
int
(
elapsed
)
"elapsed_seconds"
:
int
(
elapsed
)
}
}
def
_handle_
start_game
(
self
,
message
:
Message
):
def
_handle_
game_started
(
self
,
message
:
Message
):
"""Handle
START_GAME
message"""
"""Handle
GAME_STARTED
message"""
try
:
try
:
fixture_id
=
message
.
data
.
get
(
"fixture_id"
)
fixture_id
=
message
.
data
.
get
(
"fixture_id"
)
logger
.
info
(
f
"Received
START_GAME
message for fixture: {fixture_id}"
)
logger
.
info
(
f
"Received
GAME_STARTED
message for fixture: {fixture_id}"
)
# Get match interval from configuration
# Get match interval from configuration
match_interval
=
self
.
_get_match_interval
()
match_interval
=
self
.
_get_match_interval
()
...
@@ -171,7 +171,7 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -171,7 +171,7 @@ class MatchTimerComponent(ThreadedComponent):
self
.
_start_timer
(
match_interval
*
60
,
fixture_id
)
self
.
_start_timer
(
match_interval
*
60
,
fixture_id
)
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to handle
START_GAME
message: {e}"
)
logger
.
error
(
f
"Failed to handle
GAME_STARTED
message: {e}"
)
def
_handle_schedule_games
(
self
,
message
:
Message
):
def
_handle_schedule_games
(
self
,
message
:
Message
):
"""Handle SCHEDULE_GAMES message"""
"""Handle SCHEDULE_GAMES message"""
...
@@ -435,9 +435,8 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -435,9 +435,8 @@ class MatchTimerComponent(ThreadedComponent):
}
}
)
)
# Send to web dashboard for broadcasting to clients
# Broadcast to all components including qt_player and web_dashboard
update_message
.
recipient
=
"web_dashboard"
self
.
message_bus
.
publish
(
update_message
,
broadcast
=
True
)
self
.
message_bus
.
publish
(
update_message
)
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to send timer update: {e}"
)
logger
.
error
(
f
"Failed to send timer update: {e}"
)
\ No newline at end of file
mbetterclient/core/message_bus.py
View file @
e1efad39
...
@@ -62,6 +62,7 @@ class MessageType(Enum):
...
@@ -62,6 +62,7 @@ class MessageType(Enum):
# Game messages
# Game messages
START_GAME
=
"START_GAME"
START_GAME
=
"START_GAME"
GAME_STARTED
=
"GAME_STARTED"
SCHEDULE_GAMES
=
"SCHEDULE_GAMES"
SCHEDULE_GAMES
=
"SCHEDULE_GAMES"
START_GAME_DELAYED
=
"START_GAME_DELAYED"
START_GAME_DELAYED
=
"START_GAME_DELAYED"
START_INTRO
=
"START_INTRO"
START_INTRO
=
"START_INTRO"
...
@@ -572,6 +573,17 @@ class MessageBuilder:
...
@@ -572,6 +573,17 @@ class MessageBuilder:
}
}
)
)
@
staticmethod
def
game_started
(
sender
:
str
,
fixture_id
:
str
)
->
Message
:
"""Create GAME_STARTED message"""
return
Message
(
type
=
MessageType
.
GAME_STARTED
,
sender
=
sender
,
data
=
{
"fixture_id"
:
fixture_id
}
)
@
staticmethod
@
staticmethod
def
schedule_games
(
sender
:
str
,
fixture_id
:
Optional
[
str
]
=
None
)
->
Message
:
def
schedule_games
(
sender
:
str
,
fixture_id
:
Optional
[
str
]
=
None
)
->
Message
:
"""Create SCHEDULE_GAMES message"""
"""Create SCHEDULE_GAMES message"""
...
...
mbetterclient/database/migrations.py
View file @
e1efad39
...
@@ -2140,6 +2140,182 @@ class Migration_028_AddFixtureRefreshIntervalConfig(DatabaseMigration):
...
@@ -2140,6 +2140,182 @@ class Migration_028_AddFixtureRefreshIntervalConfig(DatabaseMigration):
logger
.
error
(
f
"Failed to remove fixture refresh interval configuration: {e}"
)
logger
.
error
(
f
"Failed to remove fixture refresh interval configuration: {e}"
)
return
False
return
False
class
Migration_029_ChangeMatchNumberToUniqueWithinFixture
(
DatabaseMigration
):
"""Change match_number from globally unique to unique within fixture"""
def
__init__
(
self
):
super
()
.
__init__
(
"029"
,
"Change match_number from globally unique to unique within fixture"
)
def
up
(
self
,
db_manager
)
->
bool
:
"""Change match_number constraint from global uniqueness to unique within fixture"""
try
:
with
db_manager
.
engine
.
connect
()
as
conn
:
# SQLite doesn't support ALTER TABLE DROP CONSTRAINT directly
# We need to recreate the table with the new constraint
# Step 1: Create new table with correct constraint
conn
.
execute
(
text
(
"""
CREATE TABLE IF NOT EXISTS matches_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
match_number INTEGER NOT NULL,
fighter1_township VARCHAR(255) NOT NULL,
fighter2_township VARCHAR(255) NOT NULL,
venue_kampala_township VARCHAR(255) NOT NULL,
start_time DATETIME NULL,
end_time DATETIME NULL,
result VARCHAR(255) NULL,
done BOOLEAN DEFAULT FALSE NOT NULL,
running BOOLEAN DEFAULT FALSE NOT NULL,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
fixture_active_time INTEGER NULL,
filename VARCHAR(1024) NOT NULL,
file_sha1sum VARCHAR(255) NOT NULL,
fixture_id VARCHAR(255) NOT NULL,
active_status BOOLEAN DEFAULT FALSE,
zip_filename VARCHAR(1024) NULL,
zip_sha1sum VARCHAR(255) NULL,
zip_upload_status VARCHAR(20) DEFAULT 'pending',
zip_upload_progress REAL DEFAULT 0.0,
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(fixture_id, match_number)
)
"""
))
# Step 2: Copy data from old table to new table
conn
.
execute
(
text
(
"""
INSERT INTO matches_new
SELECT * FROM matches
"""
))
# Step 3: Drop old table
conn
.
execute
(
text
(
"DROP TABLE matches"
))
# Step 4: Rename new table to original name
conn
.
execute
(
text
(
"ALTER TABLE matches_new RENAME TO matches"
))
# Step 5: Recreate indexes (without the old global unique constraint)
indexes
=
[
"CREATE INDEX IF NOT EXISTS ix_matches_match_number ON matches(match_number)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_fixture_id ON matches(fixture_id)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_active_status ON matches(active_status)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_file_sha1sum ON matches(file_sha1sum)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_zip_sha1sum ON matches(zip_sha1sum)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_zip_upload_status ON matches(zip_upload_status)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_created_by ON matches(created_by)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_fixture_active_time ON matches(fixture_active_time)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_done ON matches(done)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_running ON matches(running)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_status ON matches(status)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_composite ON matches(active_status, zip_upload_status, created_at)"
,
]
for
index_sql
in
indexes
:
conn
.
execute
(
text
(
index_sql
))
conn
.
commit
()
logger
.
info
(
"Changed match_number constraint from globally unique to unique within fixture"
)
return
True
except
Exception
as
e
:
logger
.
error
(
f
"Failed to change match_number constraint: {e}"
)
return
False
def
down
(
self
,
db_manager
)
->
bool
:
"""Revert match_number constraint back to globally unique"""
try
:
with
db_manager
.
engine
.
connect
()
as
conn
:
# Check if there are any duplicate match_numbers within the same fixture
# that would prevent adding back the global unique constraint
result
=
conn
.
execute
(
text
(
"""
SELECT fixture_id, match_number, COUNT(*) as count
FROM matches
GROUP BY fixture_id, match_number
HAVING COUNT(*) > 1
"""
))
duplicates
=
result
.
fetchall
()
if
duplicates
:
logger
.
error
(
f
"Cannot revert to global unique constraint - duplicate match_numbers within fixtures found: {[(row[0], row[1]) for row in duplicates]}"
)
return
False
# Recreate table with global unique constraint on match_number
conn
.
execute
(
text
(
"""
CREATE TABLE IF NOT EXISTS matches_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
match_number INTEGER NOT NULL UNIQUE,
fighter1_township VARCHAR(255) NOT NULL,
fighter2_township VARCHAR(255) NOT NULL,
venue_kampala_township VARCHAR(255) NOT NULL,
start_time DATETIME NULL,
end_time DATETIME NULL,
result VARCHAR(255) NULL,
done BOOLEAN DEFAULT FALSE NOT NULL,
running BOOLEAN DEFAULT FALSE NOT NULL,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
fixture_active_time INTEGER NULL,
filename VARCHAR(1024) NOT NULL,
file_sha1sum VARCHAR(255) NOT NULL,
fixture_id VARCHAR(255) NOT NULL,
active_status BOOLEAN DEFAULT FALSE,
zip_filename VARCHAR(1024) NULL,
zip_sha1sum VARCHAR(255) NULL,
zip_upload_status VARCHAR(20) DEFAULT 'pending',
zip_upload_progress REAL DEFAULT 0.0,
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""
))
# Copy data from old table to new table
conn
.
execute
(
text
(
"""
INSERT INTO matches_new
SELECT * FROM matches
"""
))
# Drop old table and rename new table
conn
.
execute
(
text
(
"DROP TABLE matches"
))
conn
.
execute
(
text
(
"ALTER TABLE matches_new RENAME TO matches"
))
# Recreate indexes
indexes
=
[
"CREATE INDEX IF NOT EXISTS ix_matches_match_number ON matches(match_number)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_fixture_id ON matches(fixture_id)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_active_status ON matches(active_status)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_file_sha1sum ON matches(file_sha1sum)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_zip_sha1sum ON matches(zip_sha1sum)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_zip_upload_status ON matches(zip_upload_status)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_created_by ON matches(created_by)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_fixture_active_time ON matches(fixture_active_time)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_done ON matches(done)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_running ON matches(running)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_status ON matches(status)"
,
"CREATE INDEX IF NOT EXISTS ix_matches_composite ON matches(active_status, zip_upload_status, created_at)"
,
]
for
index_sql
in
indexes
:
conn
.
execute
(
text
(
index_sql
))
conn
.
commit
()
logger
.
info
(
"Reverted match_number constraint back to globally unique"
)
return
True
except
Exception
as
e
:
logger
.
error
(
f
"Failed to revert match_number constraint: {e}"
)
return
False
# Registry of all migrations in order
# Registry of all migrations in order
MIGRATIONS
:
List
[
DatabaseMigration
]
=
[
MIGRATIONS
:
List
[
DatabaseMigration
]
=
[
Migration_001_InitialSchema
(),
Migration_001_InitialSchema
(),
...
@@ -2170,6 +2346,7 @@ MIGRATIONS: List[DatabaseMigration] = [
...
@@ -2170,6 +2346,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_026_AddExtractionStatsTable
(),
Migration_026_AddExtractionStatsTable
(),
Migration_027_AddDefaultIntroTemplatesConfig
(),
Migration_027_AddDefaultIntroTemplatesConfig
(),
Migration_028_AddFixtureRefreshIntervalConfig
(),
Migration_028_AddFixtureRefreshIntervalConfig
(),
Migration_029_ChangeMatchNumberToUniqueWithinFixture
(),
]
]
...
...
mbetterclient/database/models.py
View file @
e1efad39
...
@@ -475,11 +475,11 @@ class MatchModel(BaseModel):
...
@@ -475,11 +475,11 @@ class MatchModel(BaseModel):
Index
(
'ix_matches_created_by'
,
'created_by'
),
Index
(
'ix_matches_created_by'
,
'created_by'
),
Index
(
'ix_matches_fixture_active_time'
,
'fixture_active_time'
),
Index
(
'ix_matches_fixture_active_time'
,
'fixture_active_time'
),
Index
(
'ix_matches_composite'
,
'active_status'
,
'zip_upload_status'
,
'created_at'
),
Index
(
'ix_matches_composite'
,
'active_status'
,
'zip_upload_status'
,
'created_at'
),
UniqueConstraint
(
'
match_number'
,
name
=
'uq_matches_match_number
'
),
UniqueConstraint
(
'
fixture_id'
,
'match_number'
,
name
=
'uq_matches_fixture_match
'
),
)
)
# Core match data from fixture file
# Core match data from fixture file
match_number
=
Column
(
Integer
,
nullable
=
False
,
unique
=
True
,
comment
=
'Match # from fixture file'
)
match_number
=
Column
(
Integer
,
nullable
=
False
,
comment
=
'Match # from fixture file'
)
fighter1_township
=
Column
(
String
(
255
),
nullable
=
False
,
comment
=
'Fighter1 (Township)'
)
fighter1_township
=
Column
(
String
(
255
),
nullable
=
False
,
comment
=
'Fighter1 (Township)'
)
fighter2_township
=
Column
(
String
(
255
),
nullable
=
False
,
comment
=
'Fighter2 (Township)'
)
fighter2_township
=
Column
(
String
(
255
),
nullable
=
False
,
comment
=
'Fighter2 (Township)'
)
venue_kampala_township
=
Column
(
String
(
255
),
nullable
=
False
,
comment
=
'Venue (Kampala Township)'
)
venue_kampala_township
=
Column
(
String
(
255
),
nullable
=
False
,
comment
=
'Venue (Kampala Township)'
)
...
...
mbetterclient/qt_player/player.py
View file @
e1efad39
...
@@ -55,11 +55,18 @@ class OverlayWebChannel(QObject):
...
@@ -55,11 +55,18 @@ class OverlayWebChannel(QObject):
# Signal to receive console messages from JavaScript
# Signal to receive console messages from JavaScript
consoleMessage
=
pyqtSignal
(
str
,
str
,
int
,
str
)
# level, message, line, source
consoleMessage
=
pyqtSignal
(
str
,
str
,
int
,
str
)
# level, message, line, source
def
__init__
(
self
,
db_manager
=
None
):
def
__init__
(
self
,
db_manager
=
None
,
message_bus
=
None
):
super
()
.
__init__
()
super
()
.
__init__
()
self
.
mutex
=
QMutex
()
self
.
mutex
=
QMutex
()
self
.
overlay_data
=
{}
self
.
overlay_data
=
{}
self
.
db_manager
=
db_manager
self
.
db_manager
=
db_manager
self
.
message_bus
=
message_bus
self
.
timer_state
=
{
"running"
:
False
,
"remaining_seconds"
:
0
}
# Subscribe to timer updates if message bus is available
if
self
.
message_bus
:
self
.
message_bus
.
subscribe
(
"qt_player"
,
MessageType
.
CUSTOM
,
self
.
_handle_timer_update
)
logger
.
info
(
"OverlayWebChannel initialized"
)
logger
.
info
(
"OverlayWebChannel initialized"
)
@
pyqtSlot
(
str
)
@
pyqtSlot
(
str
)
...
@@ -203,6 +210,33 @@ class OverlayWebChannel(QObject):
...
@@ -203,6 +210,33 @@ class OverlayWebChannel(QObject):
logger
.
error
(
f
"Failed to get fixture data: {e}"
)
logger
.
error
(
f
"Failed to get fixture data: {e}"
)
return
json
.
dumps
([])
return
json
.
dumps
([])
def
_handle_timer_update
(
self
,
message
:
Message
):
"""Handle timer update messages"""
try
:
logger
.
debug
(
f
"OverlayWebChannel received message: {message.type} from {message.sender}"
)
logger
.
debug
(
f
"Message data: {message.data}"
)
if
message
.
data
.
get
(
"timer_update"
):
timer_update
=
message
.
data
[
"timer_update"
]
with
QMutexLocker
(
self
.
mutex
):
self
.
timer_state
=
timer_update
logger
.
debug
(
f
"Timer state updated: {timer_update}"
)
else
:
logger
.
debug
(
"Message does not contain timer_update"
)
except
Exception
as
e
:
logger
.
error
(
f
"Failed to handle timer update: {e}"
)
@
pyqtSlot
(
result
=
str
)
def
getTimerState
(
self
)
->
str
:
"""Provide current cached timer state to JavaScript via WebChannel"""
try
:
with
QMutexLocker
(
self
.
mutex
):
timer_state
=
self
.
timer_state
.
copy
()
logger
.
debug
(
f
"Providing cached timer state to JavaScript: {timer_state}"
)
return
json
.
dumps
(
timer_state
)
except
Exception
as
e
:
logger
.
error
(
f
"Failed to get timer state: {e}"
)
return
json
.
dumps
({
"running"
:
False
,
"remaining_seconds"
:
0
})
def
_get_fixture_data_from_games_thread
(
self
)
->
Optional
[
List
[
Dict
[
str
,
Any
]]]:
def
_get_fixture_data_from_games_thread
(
self
)
->
Optional
[
List
[
Dict
[
str
,
Any
]]]:
"""Get fixture data from the games thread"""
"""Get fixture data from the games thread"""
try
:
try
:
...
@@ -302,6 +336,7 @@ class OverlayWebChannel(QObject):
...
@@ -302,6 +336,7 @@ class OverlayWebChannel(QObject):
class
VideoProcessingWorker
(
QRunnable
):
class
VideoProcessingWorker
(
QRunnable
):
"""Background worker for video processing tasks"""
"""Background worker for video processing tasks"""
...
@@ -427,7 +462,11 @@ class OverlayWebView(QWebEngineView):
...
@@ -427,7 +462,11 @@ class OverlayWebView(QWebEngineView):
# Setup WebChannel
# Setup WebChannel
self
.
web_channel
=
QWebChannel
()
self
.
web_channel
=
QWebChannel
()
self
.
overlay_channel
=
OverlayWebChannel
(
db_manager
=
self
.
db_manager
)
# Get message bus from parent window
message_bus
=
None
if
hasattr
(
self
.
parent
(),
'_message_bus'
):
message_bus
=
self
.
parent
()
.
_message_bus
self
.
overlay_channel
=
OverlayWebChannel
(
db_manager
=
self
.
db_manager
,
message_bus
=
message_bus
)
self
.
web_channel
.
registerObject
(
"overlay"
,
self
.
overlay_channel
)
self
.
web_channel
.
registerObject
(
"overlay"
,
self
.
overlay_channel
)
page
.
setWebChannel
(
self
.
web_channel
)
page
.
setWebChannel
(
self
.
web_channel
)
...
@@ -659,15 +698,16 @@ class OverlayWebView(QWebEngineView):
...
@@ -659,15 +698,16 @@ class OverlayWebView(QWebEngineView):
from
PyQt6.QtCore
import
QTimer
from
PyQt6.QtCore
import
QTimer
QTimer
.
singleShot
(
100
,
lambda
:
self
.
_ensure_overlay_visibility_post_load
(
was_visible
))
QTimer
.
singleShot
(
100
,
lambda
:
self
.
_ensure_overlay_visibility_post_load
(
was_visible
))
# If fixtures template was loaded, the template handles its own data fetching via JavaScript
# If fixtures or match template was loaded, the template handles its own data fetching via WebChannel
if
template_name
==
"fixtures.html"
or
template_name
==
"fixtures"
:
if
template_name
==
"fixtures.html"
or
template_name
==
"fixtures"
or
template_name
==
"match.html"
or
template_name
==
"match"
:
logger
.
info
(
"Fixtures template loaded - template handles its own data fetching via JavaScript API calls"
)
template_type
=
"fixtures"
if
(
"fixtures"
in
template_name
)
else
"match"
# Send webServerBaseUrl to the fixtures template for API calls
logger
.
info
(
f
"{template_type.title()} template loaded - template handles its own data fetching via WebChannel"
)
logger
.
info
(
f
"Sending webServerBaseUrl to fixtures template: {self.web_server_url}"
)
# Send webServerBaseUrl to the template for WebChannel setup
logger
.
info
(
f
"Sending webServerBaseUrl to {template_type} template: {self.web_server_url}"
)
data_to_send
=
{
'webServerBaseUrl'
:
self
.
web_server_url
}
data_to_send
=
{
'webServerBaseUrl'
:
self
.
web_server_url
}
if
self
.
debug_overlay
:
if
self
.
debug_overlay
:
data_to_send
[
'debugMode'
]
=
True
data_to_send
[
'debugMode'
]
=
True
logger
.
info
(
"Debug mode enabled for fixtures
template"
)
logger
.
info
(
f
"Debug mode enabled for {template_type}
template"
)
self
.
update_overlay_data
(
data_to_send
)
self
.
update_overlay_data
(
data_to_send
)
# Ensure console override is active after template load
# Ensure console override is active after template load
...
@@ -2918,6 +2958,10 @@ class QtVideoPlayer(QObject):
...
@@ -2918,6 +2958,10 @@ class QtVideoPlayer(QObject):
if
self
.
debug_player
:
if
self
.
debug_player
:
logger
.
info
(
"Calling _handle_play_video_result handler"
)
logger
.
info
(
"Calling _handle_play_video_result handler"
)
self
.
_handle_play_video_result
(
message
)
self
.
_handle_play_video_result
(
message
)
elif
message
.
type
==
MessageType
.
CUSTOM
:
if
self
.
debug_player
:
logger
.
info
(
"Calling _handle_custom_message handler"
)
self
.
_handle_custom_message
(
message
)
else
:
else
:
if
self
.
debug_player
:
if
self
.
debug_player
:
logger
.
warning
(
f
"No handler for message type: {message.type.value}"
)
logger
.
warning
(
f
"No handler for message type: {message.type.value}"
)
...
@@ -3945,6 +3989,27 @@ class QtVideoPlayer(QObject):
...
@@ -3945,6 +3989,27 @@ class QtVideoPlayer(QObject):
logger
.
info
(
"QtPlayer: System status handling failed, trying to play intro directly"
)
logger
.
info
(
"QtPlayer: System status handling failed, trying to play intro directly"
)
self
.
_check_and_play_intro
()
self
.
_check_and_play_intro
()
def
_handle_custom_message
(
self
,
message
:
Message
):
"""Handle custom messages, including timer updates for WebChannel"""
try
:
# Forward timer update messages to the OverlayWebChannel
if
message
.
data
.
get
(
"timer_update"
):
logger
.
debug
(
f
"QtPlayer: Forwarding timer update to OverlayWebChannel"
)
if
hasattr
(
self
,
'window'
)
and
self
.
window
and
hasattr
(
self
.
window
,
'window_overlay'
):
overlay_view
=
self
.
window
.
window_overlay
if
isinstance
(
overlay_view
,
OverlayWebView
)
and
hasattr
(
overlay_view
,
'overlay_channel'
):
overlay_view
.
overlay_channel
.
_handle_timer_update
(
message
)
logger
.
debug
(
"QtPlayer: Timer update forwarded to OverlayWebChannel"
)
else
:
logger
.
debug
(
"QtPlayer: No OverlayWebView or overlay_channel available for timer update"
)
else
:
logger
.
debug
(
"QtPlayer: No window or window_overlay available for timer update"
)
else
:
logger
.
debug
(
f
"QtPlayer: Received custom message without timer_update: {message.data}"
)
except
Exception
as
e
:
logger
.
error
(
f
"QtPlayer: Failed to handle custom message: {e}"
)
def
_handle_web_dashboard_ready
(
self
,
message
:
Message
):
def
_handle_web_dashboard_ready
(
self
,
message
:
Message
):
"""Handle web dashboard ready messages to update server URL"""
"""Handle web dashboard ready messages to update server URL"""
try
:
try
:
...
...
mbetterclient/qt_player/templates/fixtures.html
View file @
e1efad39
...
@@ -105,13 +105,14 @@
...
@@ -105,13 +105,14 @@
border-radius
:
20px
;
border-radius
:
20px
;
padding
:
30px
;
padding
:
30px
;
max-width
:
90%
;
max-width
:
90%
;
max-height
:
8
0%
;
max-height
:
8
5%
;
/* Increased from 80% to allow more space */
overflow
-y
:
auto
;
overflow
:
visible
;
/* Changed from overflow-y: auto to visible to prevent scrollbar */
box-shadow
:
0
8px
32px
rgba
(
0
,
0
,
0
,
0.3
);
box-shadow
:
0
8px
32px
rgba
(
0
,
0
,
0
,
0.3
);
backdrop-filter
:
blur
(
10px
);
backdrop-filter
:
blur
(
10px
);
border
:
2px
solid
rgba
(
255
,
255
,
255
,
0.1
);
border
:
2px
solid
rgba
(
255
,
255
,
255
,
0.1
);
opacity
:
0
;
opacity
:
0
;
animation
:
fadeInScale
1s
ease-out
forwards
;
animation
:
fadeInScale
1s
ease-out
forwards
;
padding-bottom
:
50px
;
/* Add extra bottom padding to ensure content doesn't touch border */
}
}
.fixtures-title
{
.fixtures-title
{
...
@@ -315,24 +316,6 @@
...
@@ -315,24 +316,6 @@
}
}
}
}
/* Scrollbar styling */
.fixtures-panel
::-webkit-scrollbar
{
width
:
8px
;
}
.fixtures-panel
::-webkit-scrollbar-track
{
background
:
rgba
(
255
,
255
,
255
,
0.1
);
border-radius
:
4px
;
}
.fixtures-panel
::-webkit-scrollbar-thumb
{
background
:
rgba
(
255
,
255
,
255
,
0.3
);
border-radius
:
4px
;
}
.fixtures-panel
::-webkit-scrollbar-thumb:hover
{
background
:
rgba
(
255
,
255
,
255
,
0.5
);
}
</style>
</style>
</head>
</head>
<body>
<body>
...
@@ -770,11 +753,17 @@
...
@@ -770,11 +753,17 @@
];
];
}
}
// Find next match and start countdown
// Get timer state and start countdown
function
findNextMatchAndStartCountdown
()
{
async
function
getTimerStateAndStartCountdown
()
{
if
(
!
fixturesData
||
fixturesData
.
length
===
0
)
{
console
.
log
(
'🔍 DEBUG: getTimerStateAndStartCountdown called'
);
return
;
}
try
{
// Get timer state from WebChannel
const
timerStateJson
=
await
window
.
overlay
.
getTimerState
();
console
.
log
(
'🔍 DEBUG: Raw timer state JSON:'
,
timerStateJson
);
const
timerState
=
JSON
.
parse
(
timerStateJson
);
console
.
log
(
'🔍 DEBUG: Parsed timer state:'
,
timerState
);
// Clear any existing countdown
// Clear any existing countdown
if
(
countdownInterval
)
{
if
(
countdownInterval
)
{
...
@@ -782,34 +771,29 @@
...
@@ -782,34 +771,29 @@
countdownInterval
=
null
;
countdownInterval
=
null
;
}
}
const
now
=
new
Date
();
if
(
timerState
.
running
&&
timerState
.
remaining_seconds
>
0
)
{
let
nextMatch
=
null
;
// Timer is running, show countdown
let
earliestTime
=
null
;
nextMatchStartTime
=
new
Date
(
Date
.
now
()
+
(
timerState
.
remaining_seconds
*
1000
));
// Find the match with the earliest start time that hasn't started yet
for
(
const
match
of
fixturesData
)
{
if
(
match
.
start_time
)
{
const
startTime
=
new
Date
(
match
.
start_time
);
if
(
startTime
>
now
&&
(
!
earliestTime
||
startTime
<
earliestTime
))
{
earliestTime
=
startTime
;
nextMatch
=
match
;
}
}
}
if
(
nextMatch
&&
earliestTime
)
{
nextMatchStartTime
=
earliestTime
;
// Show next match info
// Show next match info (generic message since we don't know which match)
const
nextMatchInfo
=
document
.
getElementById
(
'nextMatchInfo'
);
const
nextMatchInfo
=
document
.
getElementById
(
'nextMatchInfo'
);
nextMatchInfo
.
textContent
=
`Next:
${
nextMatch
.
fighter1_township
||
nextMatch
.
fighter1
}
vs
${
nextMatch
.
fighter2_township
||
nextMatch
.
fighter2
}
`
;
nextMatchInfo
.
textContent
=
`Next match starting in:
`
;
nextMatchInfo
.
style
.
display
=
'block'
;
nextMatchInfo
.
style
.
display
=
'block'
;
console
.
log
(
'🔍 DEBUG: Timer countdown displayed'
);
// Start countdown
// Start countdown
updateCountdown
();
updateCountdown
();
countdownInterval
=
setInterval
(
updateCountdown
,
1000
);
countdownInterval
=
setInterval
(
updateCountdown
,
1000
);
console
.
log
(
'🔍 DEBUG: Countdown started with timer state'
);
}
else
{
}
else
{
// No upcoming matches, hide countdown
// No active timer, hide countdown
document
.
getElementById
(
'nextMatchInfo'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'countdownTimer'
).
style
.
display
=
'none'
;
console
.
log
(
'🔍 DEBUG: No active timer, countdown hidden'
);
}
}
catch
(
error
)
{
console
.
log
(
'🔍 DEBUG: Failed to get timer state:'
,
error
);
// Fallback: hide countdown
document
.
getElementById
(
'nextMatchInfo'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'nextMatchInfo'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'countdownTimer'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'countdownTimer'
).
style
.
display
=
'none'
;
}
}
...
@@ -982,8 +966,8 @@
...
@@ -982,8 +966,8 @@
fixturesContent.style.display = 'block';
fixturesContent.style.display = 'block';
debugTime('Fixtures table rendered and displayed');
debugTime('Fixtures table rendered and displayed');
//
Find next match
and start countdown
//
Get timer state
and start countdown
findNextMatch
AndStartCountdown();
getTimerState
AndStartCountdown();
debugTime('Countdown initialization completed');
debugTime('Countdown initialization completed');
}
}
...
...
mbetterclient/qt_player/templates/match.html
View file @
e1efad39
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
<head>
<head>
<meta
charset=
"utf-8"
>
<meta
charset=
"utf-8"
>
<title>
Match Overlay
</title>
<title>
Match Overlay
</title>
<script
src=
"qrc:///qtwebchannel/qwebchannel.js"
></script>
<style>
<style>
*
{
*
{
margin
:
0
;
margin
:
0
;
...
@@ -31,6 +32,61 @@
...
@@ -31,6 +32,61 @@
z-index
:
9999
;
z-index
:
9999
;
}
}
/* Debug console overlay */
#debugConsole
{
position
:
absolute
;
top
:
10px
;
right
:
10px
;
width
:
400px
;
height
:
300px
;
background
:
rgba
(
0
,
0
,
0
,
0.9
);
border
:
2px
solid
#00ff00
;
border-radius
:
8px
;
color
:
#00ff00
;
font-family
:
'Courier New'
,
monospace
;
font-size
:
11px
;
padding
:
8px
;
overflow-y
:
auto
;
z-index
:
10000
;
display
:
none
;
/* Hidden by default, shown when --debug-player */
}
#debugConsole
.show
{
display
:
block
;
}
#debugConsole
.console-header
{
font-weight
:
bold
;
border-bottom
:
1px
solid
#00ff00
;
padding-bottom
:
4px
;
margin-bottom
:
4px
;
}
#debugConsole
.console-message
{
margin
:
2px
0
;
word-wrap
:
break-word
;
}
#debugConsole
.console-timestamp
{
color
:
#ffff00
;
}
#debugConsole
.console-level-log
{
color
:
#ffffff
;
}
#debugConsole
.console-level-error
{
color
:
#ff4444
;
}
#debugConsole
.console-level-warn
{
color
:
#ffaa00
;
}
#debugConsole
.console-level-info
{
color
:
#00aaff
;
}
.overlay-container
{
.overlay-container
{
position
:
absolute
;
position
:
absolute
;
top
:
0
;
top
:
0
;
...
@@ -45,10 +101,10 @@
...
@@ -45,10 +101,10 @@
}
}
.fixtures-panel
{
.fixtures-panel
{
background
:
rgba
(
0
,
123
,
255
,
0.
40
);
background
:
rgba
(
0
,
123
,
255
,
0.
85
);
border-radius
:
20px
;
border-radius
:
20px
;
padding
:
30px
;
padding
:
30px
;
max-width
:
9
0
%
;
max-width
:
9
5
%
;
max-height
:
80%
;
max-height
:
80%
;
overflow-y
:
auto
;
overflow-y
:
auto
;
box-shadow
:
0
8px
32px
rgba
(
0
,
0
,
0
,
0.3
);
box-shadow
:
0
8px
32px
rgba
(
0
,
0
,
0
,
0.3
);
...
@@ -58,43 +114,151 @@
...
@@ -58,43 +114,151 @@
animation
:
fadeInScale
1s
ease-out
forwards
;
animation
:
fadeInScale
1s
ease-out
forwards
;
}
}
.fixtures-title
{
.match-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
flex-start
;
margin-bottom
:
30px
;
width
:
100%
;
}
.fighters-section
{
flex
:
1
;
text-align
:
left
;
display
:
flex
;
align-items
:
center
;
}
.fighter-names
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
15px
;
}
.fighter1-name
,
.fighter2-name
{
font-size
:
72px
;
font-weight
:
bold
;
color
:
white
;
color
:
white
;
font-size
:
28px
;
text-shadow
:
3px
3px
6px
rgba
(
0
,
0
,
0
,
0.7
);
line-height
:
1.1
;
}
.vs-text
{
font-size
:
48px
;
font-weight
:
bold
;
font-weight
:
bold
;
text-align
:
center
;
color
:
#ffeb3b
;
margin-bottom
:
25px
;
text-shadow
:
2px
2px
4px
rgba
(
0
,
0
,
0
,
0.8
);
text-shadow
:
2px
2px
4px
rgba
(
0
,
0
,
0
,
0.5
);
background
:
rgba
(
0
,
0
,
0
,
0.6
);
padding
:
12px
24px
;
border-radius
:
16px
;
border
:
3px
solid
#ffeb3b
;
margin
:
16px
0
;
}
.venue-timer-section
{
flex
:
1
;
text-align
:
right
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
flex-end
;
gap
:
24px
;
}
}
.fixtures-table
{
.venue-label
{
width
:
100%
;
font-size
:
36px
;
border-collapse
:
collapse
;
font-weight
:
bold
;
color
:
white
;
color
:
rgba
(
255
,
255
,
255
,
0.8
);
font-size
:
16px
;
text-shadow
:
1px
1px
2px
rgba
(
0
,
0
,
0
,
0.5
);
background
:
transparent
;
margin-bottom
:
-6px
;
}
.venue-display
{
font-size
:
144px
;
font-weight
:
bold
;
color
:
rgba
(
255
,
255
,
255
,
0.95
);
text-shadow
:
2px
2px
4px
rgba
(
0
,
0
,
0
,
0.6
);
line-height
:
1.1
;
}
}
.fixtures-table
th
{
.next-match-info
{
padding
:
15px
10px
;
text-align
:
center
;
text-align
:
center
;
background
:
rgba
(
255
,
255
,
255
,
0.1
);
color
:
rgba
(
255
,
255
,
255
,
0.9
);
font-size
:
32px
;
margin-bottom
:
10px
;
font-weight
:
bold
;
font-weight
:
bold
;
font-size
:
14px
;
text-transform
:
uppercase
;
letter-spacing
:
1px
;
border-radius
:
8px
;
margin
:
2px
;
text-shadow
:
1px
1px
2px
rgba
(
0
,
0
,
0
,
0.5
);
text-shadow
:
1px
1px
2px
rgba
(
0
,
0
,
0
,
0.5
);
}
}
.fixtures-table
td
{
.countdown-timer
{
padding
:
12px
10px
;
text-align
:
center
;
text-align
:
center
;
background
:
rgba
(
255
,
255
,
255
,
0.05
);
color
:
#ffeb3b
;
border-radius
:
6px
;
font-size
:
64px
;
margin
:
1px
;
font-weight
:
bold
;
transition
:
background-color
0.3s
ease
;
margin-bottom
:
24px
;
text-shadow
:
2px
2px
4px
rgba
(
0
,
0
,
0
,
0.8
);
background
:
rgba
(
0
,
0
,
0
,
0.3
);
padding
:
12px
24px
;
border-radius
:
16px
;
border
:
3px
solid
rgba
(
255
,
235
,
59
,
0.3
);
}
.countdown-timer.warning
{
color
:
#ff9800
;
border-color
:
rgba
(
255
,
152
,
0
,
0.5
);
}
.countdown-timer.urgent
{
color
:
#f44336
;
border-color
:
rgba
(
244
,
67
,
54
,
0.5
);
animation
:
pulse
1s
infinite
;
}
@keyframes
pulse
{
0
%
{
transform
:
scale
(
1
);
}
50
%
{
transform
:
scale
(
1.05
);
}
100
%
{
transform
:
scale
(
1
);
}
}
.outcomes-grid
{
display
:
grid
;
gap
:
10px
;
width
:
100%
;
max-width
:
1200px
;
margin-top
:
20px
;
justify-content
:
center
;
}
.outcome-cell
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
min-height
:
80px
;
}
.outcome-name
{
font-size
:
28px
;
font-weight
:
bold
;
color
:
rgba
(
255
,
255
,
255
,
0.9
);
margin-bottom
:
8px
;
text-shadow
:
1px
1px
2px
rgba
(
0
,
0
,
0
,
0.5
);
}
.outcome-value
{
font-size
:
36px
;
font-weight
:
bold
;
color
:
#ffffff
;
text-shadow
:
1px
1px
2px
rgba
(
0
,
0
,
0
,
0.5
);
}
.under-over-cell
{
background
:
rgba
(
40
,
167
,
69
,
0.2
);
border
:
1px
solid
rgba
(
40
,
167
,
69
,
0.3
);
}
.under-over-cell
.outcome-value
{
color
:
#28a745
;
}
}
.fixtures-table
tbody
tr
:hover
td
{
.fixtures-table
tbody
tr
:hover
td
{
...
@@ -251,22 +415,37 @@
...
@@ -251,22 +415,37 @@
</style>
</style>
</head>
</head>
<body>
<body>
<!-- Debug Console Overlay -->
<div
id=
"debugConsole"
>
<div
class=
"console-header"
>
🔍 JavaScript Debug Console
</div>
<div
id=
"consoleOutput"
></div>
</div>
<div
class=
"overlay-container"
>
<div
class=
"overlay-container"
>
<div
class=
"fixtures-panel"
id=
"fixturesPanel"
>
<div
class=
"fixtures-panel"
id=
"fixturesPanel"
>
<div
class=
"fixtures-title"
id=
"matchTitle"
>
Next Match
</div>
<!-- Top section with fighters and venue/timer -->
<div
class=
"match-header"
>
<div
class=
"fighters-section"
>
<div
class=
"fighter-names"
id=
"fighterNames"
>
<div
class=
"fighter1-name"
>
Fighter 1
</div>
<div
class=
"vs-text"
>
VS
</div>
<div
class=
"fighter2-name"
>
Fighter 2
</div>
</div>
</div>
<div
class=
"venue-timer-section"
>
<div
class=
"venue-label"
>
VENUE:
</div>
<div
class=
"venue-display"
id=
"matchVenue"
>
Venue
</div>
<div
class=
"venue-display"
id=
"matchVenue"
>
Venue
</div>
<div
class=
"next-match-info"
id=
"nextMatchInfo"
style=
"display: none;"
></div>
<div
class=
"countdown-timer"
id=
"countdownTimer"
style=
"display: none;"
></div>
</div>
</div>
<!-- Bottom section with odds grid -->
<div
class=
"loading-message"
id=
"loadingMessage"
style=
"display: none;"
>
Loading match data...
</div>
<div
class=
"loading-message"
id=
"loadingMessage"
style=
"display: none;"
>
Loading match data...
</div>
<div
id=
"matchContent"
style=
"display: none;"
>
<div
id=
"matchContent"
style=
"display: none;"
>
<table
class=
"fixtures-table"
id=
"outcomesTable"
>
<div
class=
"outcomes-grid"
id=
"outcomesGrid"
>
<thead>
<!-- Grid items will be populated by JavaScript -->
<tr
id=
"outcomesHeader"
>
</div>
<!-- Headers will be populated by JavaScript -->
</tr>
</thead>
<tbody
id=
"outcomesBody"
>
<!-- Content will be populated by JavaScript -->
</tbody>
</table>
</div>
</div>
<div
class=
"no-matches"
id=
"noMatches"
style=
"display: none;"
>
<div
class=
"no-matches"
id=
"noMatches"
style=
"display: none;"
>
No matches available for betting
No matches available for betting
...
@@ -279,104 +458,327 @@
...
@@ -279,104 +458,327 @@
let
overlayData
=
{};
let
overlayData
=
{};
let
fixturesData
=
null
;
let
fixturesData
=
null
;
let
outcomesData
=
null
;
let
outcomesData
=
null
;
let
countdownInterval
=
null
;
let
nextMatchStartTime
=
null
;
let
startTime
=
null
;
// Apply console.log override immediately with buffering
(
function
()
{
var
originalConsoleLog
=
console
.
log
;
var
messageBuffer
=
[];
console
.
log
=
function
(...
args
)
{
var
message
=
args
.
map
(
String
).
join
(
' '
);
if
(
window
.
overlay
&&
window
.
overlay
.
log
)
{
window
.
overlay
.
log
(
'[LOG] '
+
message
);
}
else
{
messageBuffer
.
push
(
'[LOG] '
+
message
);
}
originalConsoleLog
.
apply
(
console
,
args
);
};
// Function to flush buffer when overlay becomes available
window
.
flushConsoleBuffer
=
function
()
{
if
(
window
.
overlay
&&
window
.
overlay
.
log
)
{
messageBuffer
.
forEach
(
function
(
msg
)
{
window
.
overlay
.
log
(
msg
);
});
messageBuffer
=
[];
}
};
// Check periodically for overlay availability
var
checkInterval
=
setInterval
(
function
()
{
if
(
window
.
overlay
&&
window
.
overlay
.
log
)
{
window
.
flushConsoleBuffer
();
clearInterval
(
checkInterval
);
}
},
50
);
// Clear interval after 5 seconds to avoid infinite polling
setTimeout
(
function
()
{
clearInterval
(
checkInterval
);
},
5000
);
})();
// Test console override
console
.
log
(
'TEST: Console override applied and buffering'
);
// Debug timing helper
function
getTimestamp
()
{
return
new
Date
().
toISOString
();
}
function
debugTime
(
label
)
{
const
now
=
Date
.
now
();
const
elapsed
=
startTime
?
` (+
${
now
-
startTime
}
ms)`
:
''
;
console
.
log
(
`🔍 DEBUG [
${
getTimestamp
()}
]
${
label
}${
elapsed
}
`
);
}
// Web server configuration - will be set via WebChannel
// Web server configuration - will be set via WebChannel
let
webServerBaseUrl
=
'http://127.0.0.1:5001'
;
// Default fallback
let
webServerBaseUrl
=
'http://127.0.0.1:5001'
;
// Default fallback
let
webServerUrlReceived
=
false
;
// Debug logging function that sends messages to Qt application logs
function
debugLog
(
message
,
level
=
'info'
)
{
try
{
// Try to send to Qt WebChannel if available
if
(
typeof
qt
!==
'undefined'
&&
qt
.
webChannelTransport
)
{
// Send debug message to Qt application
if
(
window
.
sendDebugMessage
)
{
window
.
sendDebugMessage
(
`[MATCH]
${
message
}
`
);
}
}
}
catch
(
e
)
{
// Fallback to console if WebChannel not available - use original console to avoid recursion
originalConsoleLog
(
`[MATCH FALLBACK]
${
message
}
`
);
}
// Always log to console as well for browser debugging - use original to avoid recursion
originalConsoleLog
(
`🔍 DEBUG:
${
message
}
`
);
}
// Store original console.log before overriding
const
originalConsoleLog
=
console
.
log
;
// Debug console functionality
let
debugConsoleEnabled
=
false
;
const
maxConsoleMessages
=
50
;
let
consoleMessageCount
=
0
;
function
addToDebugConsole
(
level
,
message
)
{
if
(
!
debugConsoleEnabled
)
return
;
const
consoleOutput
=
document
.
getElementById
(
'consoleOutput'
);
if
(
!
consoleOutput
)
return
;
// Function to update overlay data (called by Qt WebChannel)
const
messageDiv
=
document
.
createElement
(
'div'
);
function
updateOverlayData
(
data
)
{
messageDiv
.
className
=
`console-message console-level-
${
level
}
`
;
console
.
log
(
'Received overlay data:'
,
data
);
overlayData
=
data
||
{};
// Update web server base URL if provided
const
timestamp
=
new
Date
().
toLocaleTimeString
();
messageDiv
.
innerHTML
=
`<span class="console-timestamp">[
${
timestamp
}
]</span>
${
message
}
`
;
consoleOutput
.
appendChild
(
messageDiv
);
consoleMessageCount
++
;
// Remove old messages if too many
if
(
consoleMessageCount
>
maxConsoleMessages
)
{
const
firstMessage
=
consoleOutput
.
firstElementChild
;
if
(
firstMessage
)
{
consoleOutput
.
removeChild
(
firstMessage
);
consoleMessageCount
--
;
}
}
// Auto-scroll to bottom
consoleOutput
.
scrollTop
=
consoleOutput
.
scrollHeight
;
}
// Check if debug console should be enabled (via WebChannel or timeout)
function
checkDebugConsole
()
{
const
debugConsole
=
document
.
getElementById
(
'debugConsole'
);
if
(
debugConsole
&&
debugConsole
.
classList
.
contains
(
'show'
))
{
debugConsoleEnabled
=
true
;
console
.
log
(
'🔍 Debug console detected as enabled'
);
}
}
// Check for debug console enablement once after a delay
setTimeout
(
checkDebugConsole
,
1000
);
// Setup WebChannel communication
function
setupWebChannel
()
{
// Check if WebChannel is already set up by overlay.js
if
(
window
.
overlay
)
{
console
.
log
(
'🔍 DEBUG: WebChannel already set up by overlay.js'
);
// Test WebChannel
if
(
window
.
overlay
&&
window
.
overlay
.
log
)
{
window
.
overlay
.
log
(
'TEST: WebChannel connection successful'
);
}
// Listen for data updates from Python
if
(
window
.
overlay
.
dataUpdated
)
{
window
.
overlay
.
dataUpdated
.
connect
(
function
(
data
)
{
console
.
log
(
'🔍 DEBUG: Received data update from Python:'
,
data
);
if
(
data
&&
data
.
webServerBaseUrl
)
{
if
(
data
&&
data
.
webServerBaseUrl
)
{
webServerBaseUrl
=
data
.
webServerBaseUrl
;
webServerBaseUrl
=
data
.
webServerBaseUrl
;
console
.
log
(
'Updated web server base URL:'
,
webServerBaseUrl
);
webServerUrlReceived
=
true
;
console
.
log
(
'🔍 DEBUG: Web server base URL updated to:'
,
webServerBaseUrl
);
// If we were waiting for the URL, start fetching data now
if
(
document
.
readyState
===
'complete'
||
document
.
readyState
===
'interactive'
)
{
console
.
log
(
'🔍 DEBUG: Document ready, starting data fetch after URL update'
);
fetchFixtureData
();
}
}
if
(
data
&&
data
.
debugMode
)
{
console
.
log
(
'🔍 DEBUG: Debug mode enabled, showing debug console'
);
const
debugConsole
=
document
.
getElementById
(
'debugConsole'
);
if
(
debugConsole
)
{
debugConsole
.
classList
.
add
(
'show'
);
debugConsoleEnabled
=
true
;
}
}
if
(
data
&&
data
.
timer_update
)
{
console
.
log
(
'🔍 DEBUG: Timer update received (fallback):'
,
data
.
timer_update
);
// Handle timer updates from match_timer
const
timerData
=
data
.
timer_update
;
if
(
timerData
.
running
&&
timerData
.
remaining_seconds
!==
undefined
)
{
// Format remaining time
const
minutes
=
Math
.
floor
(
timerData
.
remaining_seconds
/
60
);
const
seconds
=
timerData
.
remaining_seconds
%
60
;
const
timeString
=
`
${
minutes
.
toString
().
padStart
(
2
,
'0'
)}
:
${
seconds
.
toString
().
padStart
(
2
,
'0'
)}
`
;
const
countdownTimer
=
document
.
getElementById
(
'countdownTimer'
);
if
(
countdownTimer
)
{
countdownTimer
.
textContent
=
timeString
;
countdownTimer
.
className
=
'countdown-timer'
;
countdownTimer
.
style
.
display
=
'block'
;
// Add warning/urgent classes based on time remaining
if
(
timerData
.
remaining_seconds
<=
60
)
{
// 1 minute
countdownTimer
.
className
=
'countdown-timer urgent'
;
}
else
if
(
timerData
.
remaining_seconds
<=
300
)
{
// 5 minutes
countdownTimer
.
className
=
'countdown-timer warning'
;
}
else
{
countdownTimer
.
className
=
'countdown-timer'
;
}
}
}
}
}
if
(
data
&&
data
.
timer_update
)
{
console
.
log
(
'🔍 DEBUG: Timer update received:'
,
data
.
timer_update
);
// Handle timer updates from match_timer
const
timerData
=
data
.
timer_update
;
if
(
timerData
.
running
&&
timerData
.
remaining_seconds
!==
undefined
)
{
// Format remaining time
const
minutes
=
Math
.
floor
(
timerData
.
remaining_seconds
/
60
);
const
seconds
=
timerData
.
remaining_seconds
%
60
;
const
timeString
=
`
${
minutes
.
toString
().
padStart
(
2
,
'0'
)}
:
${
seconds
.
toString
().
padStart
(
2
,
'0'
)}
`
;
// Check if we have fixtures data
const
countdownTimer
=
document
.
getElementById
(
'countdownTimer'
);
if
(
data
&&
data
.
fixtures
)
{
if
(
countdownTimer
)
{
fixturesData
=
data
.
fixtures
;
countdownTimer
.
textContent
=
timeString
;
renderMatch
();
countdownTimer
.
className
=
'countdown-timer'
;
countdownTimer
.
style
.
display
=
'block'
;
// Add warning/urgent classes based on time remaining
if
(
timerData
.
remaining_seconds
<=
60
)
{
// 1 minute
countdownTimer
.
className
=
'countdown-timer urgent'
;
}
else
if
(
timerData
.
remaining_seconds
<=
300
)
{
// 5 minutes
countdownTimer
.
className
=
'countdown-timer warning'
;
}
else
{
}
else
{
// Fetch fixtures data from API
countdownTimer
.
className
=
'countdown-timer'
;
fetchFixturesData
().
then
(()
=>
{
}
renderMatch
();
}
}
}
});
});
}
}
return
;
}
}
// Fetch fixtures data from the API
// Fallback: setup WebChannel if overlay.js didn't do it
async
function
fetchFixturesData
(
)
{
if
(
typeof
qt
!==
'undefined'
&&
qt
.
webChannelTransport
)
{
try
{
try
{
console
.
log
(
'Fetching fixtures data from API...'
);
new
QWebChannel
(
qt
.
webChannelTransport
,
function
(
channel
)
{
console
.
log
(
'🔍 DEBUG: WebChannel connected successfully (fallback)'
);
// Try multiple API endpoints with different authentication levels
const
apiEndpoints
=
[
`
${
webServerBaseUrl
}
/api/cashier/pending-matches`
,
`
${
webServerBaseUrl
}
/api/fixtures`
,
`
${
webServerBaseUrl
}
/api/status`
// Fallback to basic status endpoint
];
let
apiData
=
null
;
let
usedEndpoint
=
null
;
for
(
const
endpoint
of
apiEndpoints
)
{
// Connect to overlay object
try
{
window
.
overlay
=
channel
.
objects
.
overlay
;
console
.
log
(
`Trying API endpoint:
${
endpoint
}
`
);
const
response
=
await
fetch
(
endpoint
,
{
method
:
'GET'
,
headers
:
{
'Content-Type'
:
'application/json'
},
credentials
:
'include'
// Include cookies for authentication
});
if
(
response
.
ok
)
{
// Listen for data updates from Python
const
data
=
await
response
.
json
();
if
(
window
.
overlay
&&
window
.
overlay
.
dataUpdated
)
{
console
.
log
(
`API Response from
${
endpoint
}
:`
,
data
);
window
.
overlay
.
dataUpdated
.
connect
(
function
(
data
)
{
console
.
log
(
'🔍 DEBUG: Received data update from Python:'
,
data
);
if
(
data
&&
data
.
webServerBaseUrl
)
{
webServerBaseUrl
=
data
.
webServerBaseUrl
;
webServerUrlReceived
=
true
;
console
.
log
(
'🔍 DEBUG: Web server base URL updated to:'
,
webServerBaseUrl
);
if
(
data
.
success
)
{
// If we were waiting for the URL, start fetching data now
apiData
=
data
;
if
(
document
.
readyState
===
'complete'
||
document
.
readyState
===
'interactive'
)
{
usedEndpoint
=
endpoint
;
console
.
log
(
'🔍 DEBUG: Document ready, starting data fetch after URL update'
)
;
break
;
fetchFixtureData
()
;
}
}
}
else
{
console
.
warn
(
`API endpoint
${
endpoint
}
returned status
${
response
.
status
}
`
);
}
}
}
catch
(
endpointError
)
{
if
(
data
&&
data
.
debugMode
)
{
console
.
warn
(
`Failed to fetch from
${
endpoint
}
:`
,
endpointError
);
console
.
log
(
'🔍 DEBUG: Debug mode enabled, showing debug console'
);
continue
;
const
debugConsole
=
document
.
getElementById
(
'debugConsole'
);
if
(
debugConsole
)
{
debugConsole
.
classList
.
add
(
'show'
);
debugConsoleEnabled
=
true
;
}
}
}
}
});
if
(
apiData
&&
apiData
.
matches
&&
apiData
.
matches
.
length
>
0
)
{
}
else
{
console
.
log
(
`Found
${
apiData
.
matches
.
length
}
matches from
${
usedEndpoint
}
`
);
console
.
log
(
'🔍 DEBUG: Overlay object not available in WebChannel'
);
fixturesData
=
apiData
.
matches
;
renderFixtures
();
return
Promise
.
resolve
();
}
else
if
(
apiData
&&
apiData
.
fixtures
&&
apiData
.
fixtures
.
length
>
0
)
{
// Handle fixtures endpoint format
console
.
log
(
`Found
${
apiData
.
fixtures
.
length
}
fixtures from
${
usedEndpoint
}
`
);
// Convert fixtures to matches format
fixturesData
=
[];
apiData
.
fixtures
.
forEach
(
fixture
=>
{
if
(
fixture
.
matches
)
{
fixturesData
.
push
(...
fixture
.
matches
);
}
}
});
});
if
(
fixturesData
.
length
>
0
)
{
}
catch
(
e
)
{
renderFixtures
();
console
.
log
(
'🔍 DEBUG: Failed to setup WebChannel:'
,
e
);
return
Promise
.
resolve
();
}
}
else
{
console
.
log
(
'🔍 DEBUG: WebChannel not available, using default webServerBaseUrl'
);
}
}
}
}
// If we reach here, no valid data was found
// Console override is now applied at the beginning of the script with buffering
console
.
log
(
'No fixture data available from any API endpoint, will show fallback'
);
return
Promise
.
reject
(
'No API data available'
);
// Fetch fixture data using WebChannel instead of API fetch
async
function
fetchFixtureData
()
{
try
{
debugTime
(
'Fetching fixture data using WebChannel'
);
console
.
log
(
'DEBUG: [MATCH] Attempting to get fixture data from WebChannel'
);
console
.
log
(
'DEBUG: [MATCH] window.overlay exists:'
,
!!
window
.
overlay
);
console
.
log
(
'DEBUG: [MATCH] window.overlay.getFixtureData exists:'
,
!!
(
window
.
overlay
&&
window
.
overlay
.
getFixtureData
));
if
(
window
.
overlay
&&
window
.
overlay
.
getFixtureData
)
{
// Get fixture data from Qt WebChannel (returns a Promise)
const
fixtureJson
=
await
window
.
overlay
.
getFixtureData
();
console
.
log
(
'DEBUG: [MATCH] Received fixture data from WebChannel:'
,
fixtureJson
);
if
(
fixtureJson
)
{
try
{
fixturesData
=
JSON
.
parse
(
fixtureJson
);
console
.
log
(
'DEBUG: [MATCH] Parsed fixture data:'
,
fixturesData
);
console
.
log
(
'DEBUG: [MATCH] fixturesData.length:'
,
fixturesData
?
fixturesData
.
length
:
'null'
);
debugTime
(
'Fixture data received from WebChannel'
);
if
(
fixturesData
&&
fixturesData
.
length
>
0
)
{
console
.
log
(
'DEBUG: [MATCH] WebChannel returned fixture data, calling renderMatch()'
);
renderMatch
();
}
else
{
console
.
log
(
'DEBUG: [MATCH] WebChannel returned empty fixture data'
);
debugTime
(
'No fixture data in WebChannel response'
);
showNoMatches
(
'No matches available'
);
}
}
catch
(
parseError
)
{
console
.
log
(
'DEBUG: [MATCH] Failed to parse fixture data from WebChannel:'
,
parseError
);
debugTime
(
`Failed to parse fixture data:
${
parseError
.
message
}
`
);
showNoMatches
(
'Unable to load matches - data parsing failed'
);
}
}
else
{
console
.
log
(
'DEBUG: [MATCH] WebChannel returned null/empty fixture data'
);
debugTime
(
'No fixture data from WebChannel'
);
showNoMatches
(
'No matches available'
);
}
}
else
{
console
.
log
(
'DEBUG: [MATCH] WebChannel overlay.getFixtureData not available'
);
console
.
log
(
'DEBUG: [MATCH] window object keys:'
,
Object
.
keys
(
window
));
if
(
window
.
overlay
)
{
console
.
log
(
'DEBUG: [MATCH] window.overlay keys:'
,
Object
.
keys
(
window
.
overlay
));
}
debugTime
(
'WebChannel not available for fixture data'
);
showNoMatches
(
'Unable to load matches - WebChannel not available'
);
}
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'Error fetching fixtures data:'
,
error
);
console
.
log
(
'DEBUG: [MATCH] Exception caught in fetchFixtureData'
);
return
Promise
.
reject
(
error
);
console
.
log
(
'DEBUG: [MATCH] Error message ='
,
error
.
message
);
console
.
log
(
'DEBUG: [MATCH] Error stack ='
,
error
.
stack
);
debugTime
(
`Failed to fetch fixture data:
${
error
.
message
}
`
);
showNoMatches
(
'Unable to load matches - WebChannel error'
);
}
}
}
}
...
@@ -474,8 +876,99 @@
...
@@ -474,8 +876,99 @@
];
];
}
}
// Render the focused match view (first match in bet status)
// Find next match and start countdown (same as fixtures.html)
function
findNextMatchAndStartCountdown
()
{
if
(
!
fixturesData
||
fixturesData
.
length
===
0
)
{
return
;
}
// Clear any existing countdown
if
(
countdownInterval
)
{
clearInterval
(
countdownInterval
);
countdownInterval
=
null
;
}
const
now
=
new
Date
();
let
nextMatch
=
null
;
let
earliestTime
=
null
;
// Find the match with the earliest start time that hasn't started yet
for
(
const
match
of
fixturesData
)
{
if
(
match
.
start_time
)
{
const
startTime
=
new
Date
(
match
.
start_time
);
if
(
startTime
>
now
&&
(
!
earliestTime
||
startTime
<
earliestTime
))
{
earliestTime
=
startTime
;
nextMatch
=
match
;
}
}
}
if
(
nextMatch
&&
earliestTime
)
{
nextMatchStartTime
=
earliestTime
;
// Show next match info
const
nextMatchInfo
=
document
.
getElementById
(
'nextMatchInfo'
);
const
fighter1
=
nextMatch
.
fighter1_township
||
nextMatch
.
fighter1
||
'Fighter 1'
;
const
fighter2
=
nextMatch
.
fighter2_township
||
nextMatch
.
fighter2
||
'Fighter 2'
;
nextMatchInfo
.
textContent
=
`Next:
${
fighter1
}
vs
${
fighter2
}
`
;
nextMatchInfo
.
style
.
display
=
'block'
;
// Start countdown
updateCountdown
();
countdownInterval
=
setInterval
(
updateCountdown
,
1000
);
}
else
{
// No upcoming matches, hide countdown
document
.
getElementById
(
'nextMatchInfo'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'countdownTimer'
).
style
.
display
=
'none'
;
}
}
// Update countdown display
function
updateCountdown
()
{
if
(
!
nextMatchStartTime
)
{
return
;
}
const
now
=
new
Date
();
const
timeDiff
=
nextMatchStartTime
-
now
;
if
(
timeDiff
<=
0
)
{
// Match has started
document
.
getElementById
(
'countdownTimer'
).
textContent
=
'LIVE'
;
document
.
getElementById
(
'countdownTimer'
).
className
=
'countdown-timer'
;
return
;
}
const
hours
=
Math
.
floor
(
timeDiff
/
(
1000
*
60
*
60
));
const
minutes
=
Math
.
floor
((
timeDiff
%
(
1000
*
60
*
60
))
/
(
1000
*
60
));
const
seconds
=
Math
.
floor
((
timeDiff
%
(
1000
*
60
))
/
1000
);
let
timeString
=
''
;
if
(
hours
>
0
)
{
timeString
=
`
${
hours
}
:
${
minutes
.
toString
().
padStart
(
2
,
'0'
)}
:
${
seconds
.
toString
().
padStart
(
2
,
'0'
)}
`
;
}
else
{
timeString
=
`
${
minutes
}
:
${
seconds
.
toString
().
padStart
(
2
,
'0'
)}
`
;
}
const
countdownTimer
=
document
.
getElementById
(
'countdownTimer'
);
countdownTimer
.
textContent
=
timeString
;
// Add warning/urgent classes based on time remaining
if
(
timeDiff
<=
60000
)
{
// 1 minute
countdownTimer
.
className
=
'countdown-timer urgent'
;
}
else
if
(
timeDiff
<=
300000
)
{
// 5 minutes
countdownTimer
.
className
=
'countdown-timer warning'
;
}
else
{
countdownTimer
.
className
=
'countdown-timer'
;
}
countdownTimer
.
style
.
display
=
'block'
;
}
// Render the focused match view (first match in bet status) - TV titling style
function
renderMatch
()
{
function
renderMatch
()
{
debugTime
(
'Starting renderMatch function'
);
const
loadingMessage
=
document
.
getElementById
(
'loadingMessage'
);
const
loadingMessage
=
document
.
getElementById
(
'loadingMessage'
);
const
matchContent
=
document
.
getElementById
(
'matchContent'
);
const
matchContent
=
document
.
getElementById
(
'matchContent'
);
const
noMatches
=
document
.
getElementById
(
'noMatches'
);
const
noMatches
=
document
.
getElementById
(
'noMatches'
);
...
@@ -486,45 +979,75 @@
...
@@ -486,45 +979,75 @@
loadingMessage
.
style
.
display
=
'none'
;
loadingMessage
.
style
.
display
=
'none'
;
noMatches
.
style
.
display
=
'none'
;
noMatches
.
style
.
display
=
'none'
;
debugTime
(
'UI elements updated - loading message hidden'
);
if
(
!
fixturesData
||
fixturesData
.
length
===
0
)
{
if
(
!
fixturesData
||
fixturesData
.
length
===
0
)
{
showNoMatches
(
'No matches available for betting'
);
debugTime
(
'No fixtures data available'
);
showNoMatches
(
'No matches available'
);
return
;
return
;
}
}
// Find the first match with status 'bet'
debugTime
(
`Processing
${
fixturesData
.
length
}
fixtures`
);
const
betMatch
=
fixturesData
.
find
(
match
=>
match
.
status
===
'bet'
);
console
.
log
(
'DEBUG: fixturesData ='
,
fixturesData
);
// Find the first match (next upcoming match)
const
nextMatch
=
fixturesData
[
0
];
// Get first match from the 5 retrieved
if
(
!
be
tMatch
)
{
if
(
!
nex
tMatch
)
{
showNoMatches
(
'No matches
currently available for betting
'
);
showNoMatches
(
'No matches
available
'
);
return
;
return
;
}
}
console
.
log
(
'Rendering focused match:'
,
betMatch
);
console
.
log
(
'Rendering next match for TV titling:'
,
nextMatch
);
console
.
log
(
'DEBUG: nextMatch properties:'
,
Object
.
keys
(
nextMatch
));
console
.
log
(
'DEBUG: nextMatch.fighter1_township ='
,
nextMatch
.
fighter1_township
);
console
.
log
(
'DEBUG: nextMatch.fighter2_township ='
,
nextMatch
.
fighter2_township
);
console
.
log
(
'DEBUG: nextMatch.venue_kampala_township ='
,
nextMatch
.
venue_kampala_township
);
// Update fighter names in top left (multi-line layout)
const
fighter1
=
nextMatch
.
fighter1_township
||
nextMatch
.
fighter1
||
'Fighter 1'
;
const
fighter2
=
nextMatch
.
fighter2_township
||
nextMatch
.
fighter2
||
'Fighter 2'
;
const
fighter1Name
=
document
.
querySelector
(
'.fighter1-name'
);
const
fighter2Name
=
document
.
querySelector
(
'.fighter2-name'
);
// Update title and venue
if
(
fighter1Name
)
fighter1Name
.
textContent
=
fighter1
;
const
fighter1
=
betMatch
.
fighter1_township
||
betMatch
.
fighter1
||
'Fighter 1'
;
if
(
fighter2Name
)
fighter2Name
.
textContent
=
fighter2
;
const
fighter2
=
betMatch
.
fighter2_township
||
betMatch
.
fighter2
||
'Fighter 2'
;
matchTitle
.
textContent
=
`
${
fighter1
}
vs
${
fighter2
}
`
;
const
venue
=
betMatch
.
venue_kampala_township
||
betMatch
.
venue
||
'TBD'
;
// Update venue in top right
const
venue
=
nextMatch
.
venue_kampala_township
||
nextMatch
.
venue
||
'TBD'
;
matchVenue
.
textContent
=
venue
;
matchVenue
.
textContent
=
venue
;
// Get outcomes for this match
// Get outcomes for this match (comes as object/dict from database)
const
outcomes
=
betMatch
.
outcomes
||
[];
let
outcomes
=
nextMatch
.
outcomes
||
{};
if
(
outcomes
.
length
===
0
)
{
console
.
log
(
'DEBUG: Raw outcomes object:'
,
outcomes
);
console
.
log
(
'DEBUG: Outcomes type:'
,
typeof
outcomes
);
console
.
log
(
'DEBUG: Outcomes keys:'
,
Object
.
keys
(
outcomes
));
// Convert outcomes object to array format for processing
let
outcomesArray
=
[];
if
(
typeof
outcomes
===
'object'
&&
outcomes
!==
null
)
{
// Handle dict format from database
Object
.
entries
(
outcomes
).
forEach
(([
key
,
value
])
=>
{
outcomesArray
.
push
({
outcome_name
:
key
,
outcome_value
:
value
});
});
}
if
(
outcomesArray
.
length
===
0
)
{
console
.
log
(
'No outcomes found for match, using defaults'
);
console
.
log
(
'No outcomes found for match, using defaults'
);
// Use default outcomes if none available
// Use default outcomes if none available
outcomes
.
push
(...
getDefaultOutcomes
()
);
outcomes
Array
=
getDefaultOutcomes
(
);
}
}
console
.
log
(
`Found
${
outcomes
.
length
}
outcomes for match
${
betMatch
.
id
||
be
tMatch
.
match_number
}
`
);
console
.
log
(
`Found
${
outcomes
Array
.
length
}
outcomes for match
${
nextMatch
.
id
||
nex
tMatch
.
match_number
}
`
);
// Sort outcomes: common ones first, then alphabetically
// Sort outcomes: common ones first, then alphabetically
const
sortedOutcomes
=
outcomes
.
sort
((
a
,
b
)
=>
{
const
sortedOutcomes
=
outcomesArray
.
sort
((
a
,
b
)
=>
{
// Handle both API formats
const
aName
=
a
.
outcome_name
||
''
;
const
aName
=
a
.
outcome_name
||
a
.
column_name
||
''
;
const
bName
=
b
.
outcome_name
||
''
;
const
bName
=
b
.
outcome_name
||
b
.
column_name
||
''
;
// Priority order for common outcomes
// Priority order for common outcomes
const
priority
=
{
const
priority
=
{
...
@@ -543,27 +1066,37 @@
...
@@ -543,27 +1066,37 @@
return
aName
.
localeCompare
(
bName
);
return
aName
.
localeCompare
(
bName
);
});
});
// Create
table header
// Create
a balanced grid where each cell contains both outcome name and value
outcomesHeader
.
innerHTML
=
sortedOutcomes
.
map
(
outcome
=>
{
// Use square grid sizing (round up to next square)
const
outcomeName
=
outcome
.
outcome_name
||
outcome
.
column_name
;
const
totalOutcomes
=
sortedOutcomes
.
length
;
return
`<th>
${
outcomeName
}
</th>`
;
const
gridSize
=
Math
.
ceil
(
Math
.
sqrt
(
totalOutcomes
))
;
}).
join
(
''
);
// Set CSS grid columns
// Create table body with odds
const
outcomesGrid
=
document
.
getElementById
(
'outcomesGrid'
);
outcomes
Body
.
innerHTML
=
`
outcomes
Grid
.
style
.
gridTemplateColumns
=
`repeat(
${
gridSize
}
, 1fr)`
;
<tr>
${
sortedOutcomes
.
map
(
outcome
=>
{
let
gridHTML
=
''
;
const
outcomeName
=
outcome
.
outcome_name
||
outcome
.
column_name
;
const
outcomeValue
=
outcome
.
outcome_value
||
outcome
.
float_value
;
// Create grid items for each outcome
const
isUnderOver
=
outcomeName
===
'UNDER'
||
outcomeName
===
'OVER'
;
sortedOutcomes
.
forEach
(
outcome
=>
{
const
oddsClass
=
isUnderOver
?
'odds-value under-over'
:
'odds-value'
;
const
outcomeValue
=
outcome
.
outcome_value
;
const
displayValue
=
outcomeValue
!==
undefined
&&
outcomeValue
!==
null
?
parseFloat
(
outcomeValue
).
toFixed
(
2
)
:
'-'
;
const
displayValue
=
outcomeValue
!==
undefined
&&
outcomeValue
!==
null
?
parseFloat
(
outcomeValue
).
toFixed
(
2
)
:
'-'
;
return
`<td><span class="
${
oddsClass
}
">
${
displayValue
}
</span></td>`
;
const
isUnderOver
=
outcome
.
outcome_name
===
'UNDER'
||
outcome
.
outcome_name
===
'OVER'
;
}).
join
(
''
)}
const
cellClass
=
isUnderOver
?
'outcome-cell under-over-cell'
:
'outcome-cell'
;
<
/tr
>
gridHTML
+=
`<div class="
${
cellClass
}
">
`;
<div class="outcome-name">
${
outcome
.
outcome_name
}
</div>
<div class="outcome-value">
${
displayValue
}
</div>
</div>`
;
});
outcomesGrid
.
innerHTML
=
gridHTML
;
matchContent
.
style
.
display
=
'block'
;
matchContent
.
style
.
display
=
'block'
;
debugTime
(
'Match rendered and displayed'
);
// Find next match and start countdown
findNextMatchAndStartCountdown
();
debugTime
(
'Countdown initialization completed'
);
}
}
// Show no matches message
// Show no matches message
...
@@ -577,60 +1110,48 @@
...
@@ -577,60 +1110,48 @@
// Initialize when DOM is loaded
// Initialize when DOM is loaded
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
console.log('Match overlay initialized - attempting to fetch next match data');
startTime
=
Date
.
now
();
debugTime
(
'DOM Content Loaded - Starting match overlay initialization'
);
// Setup WebChannel first
setupWebChannel
();
// Show loading message initially
// Show loading message initially
document.getElementById('
fixtures
Content').style.display = 'none';
document
.
getElementById
(
'
match
Content'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'noMatches'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'noMatches'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'loadingMessage'
).
style
.
display
=
'block'
;
document
.
getElementById
(
'loadingMessage'
).
style
.
display
=
'block'
;
document
.
getElementById
(
'loadingMessage'
).
textContent
=
'Loading next match data...'
;
document
.
getElementById
(
'loadingMessage'
).
textContent
=
'Loading next match data...'
;
// Start fetching real data immediately
debugTime
(
'UI initialized - Loading message displayed'
);
fetchFixturesData().then(() => {
renderMatch();
// Wait briefly for WebChannel to connect and potentially receive webServerBaseUrl
// If API fails completely, show fallback data after a short delay
setTimeout
(()
=>
{
setTimeout
(()
=>
{
if (!fixturesData || fixturesData.length === 0) {
if
(
!
webServerUrlReceived
)
{
console.log('No data loaded after API attempts, forcing fallback display');
console
.
log
(
'🔍 DEBUG: WebServerBaseUrl not received via WebChannel, proceeding with WebChannel data fetch'
);
showFallbackMatches();
renderMatch();
}
}
}, 2000);
}).catch(() => {
console.log('API fetch failed, showing fallback data');
showFallbackMatches();
renderMatch();
});
// Refresh data every 30 seconds
setInterval(function() {
console.log('Refreshing match data...');
fetchFixturesData().then(() => {
renderMatch();
});
}, 30000);
});
// Qt WebChannel initialization (when available)
// Fetch fixture data directly from WebChannel
if (typeof QWebChannel !== 'undefined') {
debugTime
(
'Fetching fixture data from WebChannel'
);
new QWebChannel(qt.webChannelTransport, function(channel) {
fetchFixtureData
();
console.log('WebChannel initialized for match overlay');
// Connect to overlay object if available
// Set up periodic refresh every 30 seconds
if (channel.objects.overlay) {
setTimeout
(
function
refreshData
()
{
channel.objects.overlay.dataChanged.connect(function(data) {
debugTime
(
'Periodic refresh: fetching updated fixture data'
);
updateOverlayData(data);
fetchFixtureData
();
});
setTimeout
(
refreshData
,
30000
);
},
30000
);
// Get initial data
// Show no matches if no data after 5 seconds total
if (channel.objects.overlay.getCurrentData) {
setTimeout
(()
=>
{
channel.objects.overlay.getCurrentData(function(data) {
if
(
!
fixturesData
||
fixturesData
.
length
===
0
)
{
updateOverlayData(data);
debugTime
(
'No data received after 5 seconds - showing waiting message'
);
});
showNoMatches
(
'Waiting for match data...'
);
}
}
else
{
debugTime
(
'Data was received before 5 second timeout'
);
}
}
},
5000
);
},
50
);
// Wait 50ms for WebChannel setup
});
});
}
</script>
</script>
<!--
<!--
...
...
mbetterclient/web_dashboard/routes.py
View file @
e1efad39
...
@@ -2613,7 +2613,7 @@ def notifications():
...
@@ -2613,7 +2613,7 @@ def notifications():
def
message_handler
(
message
):
def
message_handler
(
message
):
"""Handle incoming messages for this client"""
"""Handle incoming messages for this client"""
if
message
.
type
in
[
MessageType
.
START_GAME
,
MessageType
.
MATCH_START
,
MessageType
.
GAME_STATUS
]:
if
message
.
type
in
[
MessageType
.
START_GAME
,
MessageType
.
GAME_STARTED
,
MessageType
.
MATCH_START
,
MessageType
.
GAME_STATUS
]:
notification_data
=
{
notification_data
=
{
"type"
:
message
.
type
.
value
,
"type"
:
message
.
type
.
value
,
"data"
:
message
.
data
,
"data"
:
message
.
data
,
...
...
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