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
Expand all
Hide 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
(
f
"Found {len(stale_matches)} stale ingame matches - cleaning up"
)
for
match
in
stale_matches
:
logger
.
info
(
f
"Cleaning up stale match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township}"
)
match
.
status
=
'pending'
match
.
active_status
=
False
session
.
commit
()
logger
.
info
(
f
"Cleaned up {len(stale_matches)} stale ingame matches"
)
else
:
logger
.
info
(
"No stale ingame matches found"
)
logger
.
info
(
"No stale ingame matches found"
)
return
logger
.
info
(
f
"Found {len(stale_matches)} stale ingame matches - cleaning up"
)
# 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"
)
# Change status to pending and set active_status to False
for
match
in
old_bet_matches
:
for
match
in
stale_matches
:
logger
.
info
(
f
"Cancelling old bet 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
=
'cancelled'
match
.
status
=
'pending'
match
.
active_status
=
False
session
.
commit
()
# Cancel/refund associated bets
logger
.
info
(
f
"Cleaned up {len(stale_matches)} stale ingame matches"
)
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,46 +753,47 @@
...
@@ -770,46 +753,47 @@
];
];
}
}
// 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
;
}
// Clear any existing countdown
try
{
if
(
countdownInterval
)
{
// Get timer state from WebChannel
c
learInterval
(
countdownInterval
);
c
onst
timerStateJson
=
await
window
.
overlay
.
getTimerState
(
);
co
untdownInterval
=
null
;
co
nsole
.
log
(
'🔍 DEBUG: Raw timer state JSON:'
,
timerStateJson
)
;
}
const
timerState
=
JSON
.
parse
(
timerStateJson
);
const
now
=
new
Date
();
console
.
log
(
'🔍 DEBUG: Parsed timer state:'
,
timerState
);
let
nextMatch
=
null
;
let
earliestTime
=
null
;
// Clear any existing countdown
if
(
countdownInterval
)
{
// Find the match with the earliest start time that hasn't started yet
clearInterval
(
countdownInterval
);
for
(
const
match
of
fixturesData
)
{
countdownInterval
=
null
;
if
(
match
.
start_time
)
{
const
startTime
=
new
Date
(
match
.
start_time
);
if
(
startTime
>
now
&&
(
!
earliestTime
||
startTime
<
earliestTime
))
{
earliestTime
=
startTime
;
nextMatch
=
match
;
}
}
}
}
if
(
nextMatch
&&
earliestTime
)
{
if
(
timerState
.
running
&&
timerState
.
remaining_seconds
>
0
)
{
nextMatchStartTime
=
earliestTime
;
// Timer is running, show countdown
nextMatchStartTime
=
new
Date
(
Date
.
now
()
+
(
timerState
.
remaining_seconds
*
1000
));
// 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
);
}
else
{
console
.
log
(
'🔍 DEBUG: Countdown started with timer state'
);
// No upcoming matches, hide countdown
}
else
{
// 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
This diff is collapsed.
Click to expand it.
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