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
a1afa146
Commit
a1afa146
authored
Jan 27, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
New limits CAP
parent
2d93850e
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
427 additions
and
51 deletions
+427
-51
EXTRACTION_ALGORITHM.md
EXTRACTION_ALGORITHM.md
+8
-6
games_thread.py
mbetterclient/core/games_thread.py
+42
-37
migrations.py
mbetterclient/database/migrations.py
+161
-0
models.py
mbetterclient/database/models.py
+8
-8
routes.py
mbetterclient/web_dashboard/routes.py
+94
-0
index.html
mbetterclient/web_dashboard/templates/dashboard/index.html
+114
-0
No files found.
EXTRACTION_ALGORITHM.md
View file @
a1afa146
...
...
@@ -189,14 +189,16 @@ Store comprehensive extraction metrics in `extraction_stats` table:
-
CAP applied status
-
Result breakdown
#### Step 6.3:
Shortfall
Tracking
#### Step 6.3:
Redistribution Adjustment
Tracking
**Expected Redistribution = Total Payin × CAP Percentage**
**Actual Redistribution = Sum of all win_amounts**
**Shortfall = max(0, Expected - Actual)**
**Adjustment = Expected - Actual**
-
Positive: Under-redistribution (shortfall) - accumulated to increase future CAP
-
Negative: Over-redistribution (surplus) - accumulated to decrease future CAP
Update daily
shortfall
tracking for future CAP adjustments.
Update daily
redistribution adjustment
tracking for future CAP adjustments.
### Phase 7: System Notifications
...
...
@@ -216,7 +218,7 @@ Send `MATCH_DONE` message to advance to next match.
-
**Purpose**
: Prevent excessive payouts that could harm profitability
-
**Mechanism**
: Only select results where total payout ≤ CAP threshold
-
**Adjustment Factors**
:
-
Accumulated
shortfall
s from previous extractions
-
Accumulated
redistribution adjustment
s from previous extractions
-
Committed UNDER/OVER payouts
-
Total intake from all betting activity
...
...
@@ -228,7 +230,7 @@ Send `MATCH_DONE` message to advance to next match.
### Profit Maximization
-
**Weighted Selection**
: Prefers results maximizing redistribution
-
**Fallback Protection**
: Always selects result, even if CAP exceeded
-
**
Shortfall Carryover**
: Tracks and compensates for under-redistribution
-
**
Adjustment Carryover**
: Tracks and compensates for redistribution imbalances (both under and over)
### Data Integrity
-
**Transaction Safety**
: All updates in database transactions
...
...
@@ -238,7 +240,7 @@ Send `MATCH_DONE` message to advance to next match.
## Configuration Parameters
-
**CAP Percentage**
: Default 70%, configurable via
`extraction_redistribution_cap`
-
**
Shortfall Tracking**
: Daily accumulated shortfall
s affect future CAP calculations
-
**
Adjustment Tracking**
: Daily accumulated redistribution adjustment
s affect future CAP calculations
-
**Result Associations**
: Configurable via
`extraction_associations`
table
-
**Betting Mode**
: Affects match status progression but not extraction logic
...
...
mbetterclient/core/games_thread.py
View file @
a1afa146
...
...
@@ -12,7 +12,7 @@ from typing import Optional, Dict, Any, List
from
.thread_manager
import
ThreadedComponent
from
.message_bus
import
MessageBus
,
Message
,
MessageType
,
MessageBuilder
from
..database.manager
import
DatabaseManager
from
..database.models
import
MatchModel
,
MatchStatus
,
BetDetailModel
,
MatchOutcomeModel
,
GameConfigModel
,
ExtractionAssociationModel
,
DailyRedistributionShortfall
Model
,
MatchTemplateModel
,
MatchOutcomeTemplateModel
from
..database.models
import
MatchModel
,
MatchStatus
,
BetDetailModel
,
MatchOutcomeModel
,
GameConfigModel
,
ExtractionAssociationModel
,
PersistentRedistributionAdjustment
Model
,
MatchTemplateModel
,
MatchOutcomeTemplateModel
from
..utils.timezone_utils
import
get_today_venue_date
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -2046,60 +2046,65 @@ class GamesThread(ThreadedComponent):
logger
.
error
(
f
"Failed to get redistribution CAP: {e}"
)
return
70.0
def
_get_daily_
shortfall
(
self
,
date
,
session
)
->
float
:
"""Get accumulated
shortfall for a specific date
"""
def
_get_daily_
redistribution_adjustment
(
self
,
date
,
session
)
->
float
:
"""Get accumulated
redistribution adjustment (persistent across application restarts)
"""
try
:
shortfall_record
=
session
.
query
(
DailyRedistributionShortfallModel
)
.
filter_by
(
date
=
date
# Get the most recent accumulated_shortfall value from any record
latest_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
.
order_by
(
PersistentRedistributionAdjustmentModel
.
updated_at
.
desc
()
)
.
first
()
if
shortfall
_record
:
logger
.
debug
(
f
"Found
shortfall record for {date}: {shortfall
_record.accumulated_shortfall}"
)
return
shortfall
_record
.
accumulated_shortfall
if
latest
_record
:
logger
.
debug
(
f
"Found
persistent redistribution adjustment: {latest
_record.accumulated_shortfall}"
)
return
latest
_record
.
accumulated_shortfall
else
:
logger
.
debug
(
f
"No shortfall record found for {date}
, returning 0.0"
)
logger
.
debug
(
"No redistribution adjustment records found
, returning 0.0"
)
return
0.0
except
Exception
as
e
:
logger
.
error
(
f
"Failed to get
daily shortfall for {date}
: {e}"
)
logger
.
error
(
f
"Failed to get
persistent redistribution adjustment
: {e}"
)
return
0.0
def
_update_daily_
shortfall
(
self
,
date
,
payin_amount
,
redistributed_amount
,
cap_percentage
,
session
):
"""Update
daily shortfall
tracking after extraction"""
def
_update_daily_
redistribution_adjustment
(
self
,
date
,
payin_amount
,
redistributed_amount
,
cap_percentage
,
session
):
"""Update
persistent redistribution adjustment
tracking after extraction"""
try
:
# Calculate the shortfall for this extraction
# Calculate the redistribution adjustment for this extraction
# Positive: under-redistribution (shortfall), Negative: over-redistribution (surplus)
expected_redistribution
=
payin_amount
*
(
cap_percentage
/
100.0
)
shortfall
=
max
(
0
,
expected_redistribution
-
redistributed_amount
)
adjustment
=
expected_redistribution
-
redistributed_amount
logger
.
info
(
f
"💰 [
SHORTFALL DEBUG] Payin: {payin_amount:.2f}, Expected: {expected_redistribution:.2f}, Redistributed: {redistributed_amount:.2f}, Shortfall: {shortfall
:.2f}"
)
logger
.
info
(
f
"💰 [
ADJUSTMENT DEBUG] Payin: {payin_amount:.2f}, Expected: {expected_redistribution:.2f}, Redistributed: {redistributed_amount:.2f}, Adjustment: {adjustment
:.2f}"
)
# Get or create the daily record
shortfall_record
=
session
.
query
(
DailyRedistributionShortfallModel
)
.
filter_by
(
date
=
date
# Use a fixed date for the global persistent record
global_date
=
datetime
.
date
(
1970
,
1
,
1
)
# Fixed date for global record
# Get or create the global record
adjustment_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
.
filter_by
(
date
=
global_date
)
.
first
()
if
not
shortfall
_record
:
shortfall_record
=
DailyRedistributionShortfall
Model
(
date
=
date
,
accumulated_shortfall
=
shortfall
,
if
not
adjustment
_record
:
adjustment_record
=
PersistentRedistributionAdjustment
Model
(
date
=
global_
date
,
accumulated_shortfall
=
adjustment
,
total_payin
=
payin_amount
,
total_redistributed
=
redistributed_amount
,
cap_percentage
=
cap_percentage
)
session
.
add
(
shortfall
_record
)
logger
.
info
(
f
"Created
new shortfall record for {date} with shortfall {shortfall
:.2f}"
)
session
.
add
(
adjustment
_record
)
logger
.
info
(
f
"Created
global redistribution adjustment record with adjustment {adjustment
:.2f}"
)
else
:
# Update existing record
shortfall_record
.
accumulated_shortfall
+=
shortfall
shortfall
_record
.
total_payin
+=
payin_amount
shortfall
_record
.
total_redistributed
+=
redistributed_amount
shortfall
_record
.
cap_percentage
=
cap_percentage
# Update to latest
logger
.
info
(
f
"Updated
shortfall record for {date}, new accumulated shortfall: {shortfall
_record.accumulated_shortfall:.2f}"
)
# Update existing
global
record
adjustment_record
.
accumulated_shortfall
+=
adjustment
adjustment
_record
.
total_payin
+=
payin_amount
adjustment
_record
.
total_redistributed
+=
redistributed_amount
adjustment
_record
.
cap_percentage
=
cap_percentage
# Update to latest
logger
.
info
(
f
"Updated
global redistribution adjustment record, new accumulated adjustment: {adjustment
_record.accumulated_shortfall:.2f}"
)
session
.
commit
()
except
Exception
as
e
:
logger
.
error
(
f
"Failed to update
daily shortfall for {date}
: {e}"
)
logger
.
error
(
f
"Failed to update
persistent redistribution adjustment
: {e}"
)
session
.
rollback
()
def
_weighted_random_selection
(
self
,
under_coeff
:
float
,
over_coeff
:
float
)
->
str
:
...
...
@@ -2580,10 +2585,10 @@ class GamesThread(ThreadedComponent):
# Get redistribution CAP
cap_percentage
=
self
.
_get_redistribution_cap
()
# Get accumulated
shortfall
from previous extractions for today
# Get accumulated
redistribution adjustment
from previous extractions for today
today
=
self
.
_get_today_venue_date
()
accumulated_shortfall
=
self
.
_get_daily_
shortfall
(
today
,
session
)
logger
.
info
(
f
"🎯 [EXTRACTION DEBUG] Accumulated
shortfall
: {accumulated_shortfall:.2f}"
)
accumulated_shortfall
=
self
.
_get_daily_
redistribution_adjustment
(
today
,
session
)
logger
.
info
(
f
"🎯 [EXTRACTION DEBUG] Accumulated
redistribution adjustment
: {accumulated_shortfall:.2f}"
)
# Calculate base CAP threshold using ALL bets (UNDER/OVER + other bets)
total_payin_all_bets
=
total_payin
+
total_bet_amount
...
...
@@ -2660,10 +2665,10 @@ class GamesThread(ThreadedComponent):
logger
.
info
(
f
"📈 [EXTRACTION DEBUG] Step 9: Collecting match statistics"
)
self
.
_collect_match_statistics
(
match_id
,
fixture_id
,
selected_result
,
session
)
# Step 10: Update daily
shortfall
tracking
logger
.
info
(
f
"💰 [EXTRACTION DEBUG] Step 10: Updating daily
shortfall
tracking"
)
# Step 10: Update daily
redistribution adjustment
tracking
logger
.
info
(
f
"💰 [EXTRACTION DEBUG] Step 10: Updating daily
redistribution adjustment
tracking"
)
today
=
self
.
_get_today_venue_date
()
self
.
_update_daily_
shortfall
(
today
,
total_payin_all_bets
,
payouts
[
selected_result
],
cap_percentage
,
session
)
self
.
_update_daily_
redistribution_adjustment
(
today
,
total_payin_all_bets
,
payouts
[
selected_result
],
cap_percentage
,
session
)
logger
.
info
(
f
"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}"
)
...
...
mbetterclient/database/migrations.py
View file @
a1afa146
...
...
@@ -2731,6 +2731,165 @@ class Migration_035_AddDailyRedistributionShortfallTable(DatabaseMigration):
return
False
class
Migration_037_RenameDailyRedistributionShortfallTable
(
DatabaseMigration
):
"""Rename daily_redistribution_shortfall table to persistent_redistribution_adjustment"""
def
__init__
(
self
):
super
()
.
__init__
(
"037"
,
"Rename daily_redistribution_shortfall table to persistent_redistribution_adjustment"
)
def
up
(
self
,
db_manager
)
->
bool
:
"""Rename the table"""
try
:
with
db_manager
.
engine
.
connect
()
as
conn
:
# Check if the old table exists
result
=
conn
.
execute
(
text
(
"""
SELECT name FROM sqlite_master
WHERE type='table' AND name='daily_redistribution_shortfall'
"""
))
old_table_exists
=
result
.
fetchone
()
is
not
None
if
old_table_exists
:
# Rename the table
conn
.
execute
(
text
(
"""
ALTER TABLE daily_redistribution_shortfall
RENAME TO persistent_redistribution_adjustment
"""
))
# Note: SQLite doesn't support ALTER INDEX RENAME, so we'll leave the old index name
# The index will be recreated with the new name when the table is recreated in future migrations
conn
.
commit
()
logger
.
info
(
"Renamed daily_redistribution_shortfall table to persistent_redistribution_adjustment"
)
else
:
logger
.
info
(
"daily_redistribution_shortfall table does not exist, nothing to rename"
)
return
True
except
Exception
as
e
:
logger
.
error
(
f
"Failed to rename daily_redistribution_shortfall table: {e}"
)
return
False
def
down
(
self
,
db_manager
)
->
bool
:
"""Rename back to the old table name"""
try
:
with
db_manager
.
engine
.
connect
()
as
conn
:
# Check if the new table exists
result
=
conn
.
execute
(
text
(
"""
SELECT name FROM sqlite_master
WHERE type='table' AND name='persistent_redistribution_adjustment'
"""
))
new_table_exists
=
result
.
fetchone
()
is
not
None
if
new_table_exists
:
# Rename back to the old name
conn
.
execute
(
text
(
"""
ALTER TABLE persistent_redistribution_adjustment
RENAME TO daily_redistribution_shortfall
"""
))
# Note: SQLite doesn't support ALTER INDEX RENAME, so we'll leave the index name as-is
conn
.
commit
()
logger
.
info
(
"Renamed persistent_redistribution_adjustment table back to daily_redistribution_shortfall"
)
else
:
logger
.
info
(
"persistent_redistribution_adjustment table does not exist, nothing to rename back"
)
return
True
except
Exception
as
e
:
logger
.
error
(
f
"Failed to rename back to daily_redistribution_shortfall table: {e}"
)
return
False
class
Migration_038_AddWin1Win2Associations
(
DatabaseMigration
):
"""Add WIN1 and WIN2 result options and extraction associations for X1, X2, and 12 outcomes"""
def
__init__
(
self
):
super
()
.
__init__
(
"038"
,
"Add WIN1 and WIN2 result options and extraction associations for X1, X2, and 12 outcomes"
)
def
up
(
self
,
db_manager
)
->
bool
:
"""Add WIN1 and WIN2 result options and associations if they don't exist"""
try
:
with
db_manager
.
engine
.
connect
()
as
conn
:
# Add WIN1 and WIN2 to result_options table
result_options
=
[
(
'WIN1'
,
'Fighter 1 wins result'
,
9
),
(
'WIN2'
,
'Fighter 2 wins result'
,
10
)
]
for
result_name
,
description
,
sort_order
in
result_options
:
conn
.
execute
(
text
(
"""
INSERT OR IGNORE INTO result_options
(result_name, description, is_active, sort_order, created_at, updated_at)
VALUES (:result_name, :description, 1, :sort_order, datetime('now'), datetime('now'))
"""
),
{
'result_name'
:
result_name
,
'description'
:
description
,
'sort_order'
:
sort_order
})
# Add WIN1 associations: X1 and 12 -> WIN1
associations
=
[
(
'X1'
,
'WIN1'
),
(
'12'
,
'WIN1'
),
(
'X2'
,
'WIN2'
),
(
'12'
,
'WIN2'
)
]
for
outcome_name
,
extraction_result
in
associations
:
conn
.
execute
(
text
(
"""
INSERT OR IGNORE INTO extraction_associations
(outcome_name, extraction_result, is_default, created_at, updated_at)
VALUES (:outcome_name, :extraction_result, 1, datetime('now'), datetime('now'))
"""
),
{
'outcome_name'
:
outcome_name
,
'extraction_result'
:
extraction_result
})
conn
.
commit
()
logger
.
info
(
"Added WIN1 and WIN2 result options and extraction associations"
)
return
True
except
Exception
as
e
:
logger
.
error
(
f
"Failed to add WIN1 and WIN2 result options and associations: {e}"
)
return
False
def
down
(
self
,
db_manager
)
->
bool
:
"""Remove the WIN1 and WIN2 result options and associations added by this migration"""
try
:
with
db_manager
.
engine
.
connect
()
as
conn
:
# Remove WIN1 and WIN2 from result_options
conn
.
execute
(
text
(
"""
DELETE FROM result_options
WHERE result_name IN ('WIN1', 'WIN2')
"""
))
# Remove the specific associations added by this migration
associations
=
[
(
'X1'
,
'WIN1'
),
(
'12'
,
'WIN1'
),
(
'X2'
,
'WIN2'
),
(
'12'
,
'WIN2'
)
]
for
outcome_name
,
extraction_result
in
associations
:
conn
.
execute
(
text
(
"""
DELETE FROM extraction_associations
WHERE outcome_name = :outcome_name
AND extraction_result = :extraction_result
AND is_default = 1
"""
),
{
'outcome_name'
:
outcome_name
,
'extraction_result'
:
extraction_result
})
conn
.
commit
()
logger
.
info
(
"Removed WIN1 and WIN2 result options and extraction associations"
)
return
True
except
Exception
as
e
:
logger
.
error
(
f
"Failed to remove WIN1 and WIN2 result options and associations: {e}"
)
return
False
class
Migration_036_AddMatchTemplatesTables
(
DatabaseMigration
):
"""Add matches_templates and match_outcomes_templates tables for storing match templates"""
...
...
@@ -2886,6 +3045,8 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_034_AddDefaultLicenseText
(),
Migration_035_AddDailyRedistributionShortfallTable
(),
Migration_036_AddMatchTemplatesTables
(),
Migration_037_RenameDailyRedistributionShortfallTable
(),
Migration_038_AddWin1Win2Associations
(),
]
...
...
mbetterclient/database/models.py
View file @
a1afa146
...
...
@@ -947,22 +947,22 @@ class ExtractionStatsModel(BaseModel):
return
f
'<ExtractionStats Match {self.match_id}: {self.total_bets} bets, {self.total_amount_collected:.2f} collected, {self.actual_result}>'
class
DailyRedistributionShortfall
Model
(
BaseModel
):
"""
Daily redistribution shortfall tracking for CAP adjustmen
ts"""
__tablename__
=
'
daily_redistribution_shortfall
'
class
PersistentRedistributionAdjustment
Model
(
BaseModel
):
"""
Persistent redistribution adjustment tracking across application restar
ts"""
__tablename__
=
'
persistent_redistribution_adjustments
'
__table_args__
=
(
Index
(
'ix_
daily_redistribution_shortfall
_date'
,
'date'
),
UniqueConstraint
(
'date'
,
name
=
'uq_
daily_redistribution_shortfall
_date'
),
Index
(
'ix_
persistent_redistribution_adjustments
_date'
,
'date'
),
UniqueConstraint
(
'date'
,
name
=
'uq_
persistent_redistribution_adjustments
_date'
),
)
date
=
Column
(
Date
,
nullable
=
False
,
unique
=
True
,
comment
=
'Date for
shortfall
tracking'
)
accumulated_shortfall
=
Column
(
Float
(
precision
=
2
),
default
=
0.0
,
nullable
=
False
,
comment
=
'Accumulated
shortfall
from previous extractions'
)
date
=
Column
(
Date
,
nullable
=
False
,
unique
=
True
,
comment
=
'Date for
adjustment
tracking'
)
accumulated_shortfall
=
Column
(
Float
(
precision
=
2
),
default
=
0.0
,
nullable
=
False
,
comment
=
'Accumulated
adjustment
from previous extractions'
)
total_payin
=
Column
(
Float
(
precision
=
2
),
default
=
0.0
,
nullable
=
False
,
comment
=
'Total payin for the day'
)
total_redistributed
=
Column
(
Float
(
precision
=
2
),
default
=
0.0
,
nullable
=
False
,
comment
=
'Total redistributed for the day'
)
cap_percentage
=
Column
(
Float
(
precision
=
2
),
default
=
70.0
,
nullable
=
False
,
comment
=
'CAP percentage used for calculations'
)
def
__repr__
(
self
):
return
f
'<
DailyRedistributionShortfall {self.date}: shortfall
={self.accumulated_shortfall:.2f}, payin={self.total_payin:.2f}, redistributed={self.total_redistributed:.2f}>'
return
f
'<
PersistentRedistributionAdjustment {self.date}: adjustment
={self.accumulated_shortfall:.2f}, payin={self.total_payin:.2f}, redistributed={self.total_redistributed:.2f}>'
class
MatchTemplateModel
(
BaseModel
):
...
...
mbetterclient/web_dashboard/routes.py
View file @
a1afa146
...
...
@@ -4108,6 +4108,100 @@ def save_redistribution_cap():
return
jsonify
({
"error"
:
str
(
e
)}),
500
# Redistribution Balance API routes (admin-only)
@
api_bp
.
route
(
'/redistribution-balance'
)
@
get_api_auth_decorator
()
@
get_api_auth_decorator
(
require_admin
=
True
)
def
get_redistribution_balance
():
"""Get current redistribution balance (admin only)"""
try
:
from
..database.models
import
PersistentRedistributionAdjustmentModel
from
datetime
import
date
session
=
api_bp
.
db_manager
.
get_session
()
try
:
# Get the latest redistribution adjustment record
latest_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
\
.
order_by
(
PersistentRedistributionAdjustmentModel
.
date
.
desc
())
\
.
first
()
current_balance
=
0.0
if
latest_record
:
current_balance
=
float
(
latest_record
.
accumulated_shortfall
)
return
jsonify
({
"success"
:
True
,
"redistribution_balance"
:
current_balance
,
"last_updated"
:
latest_record
.
date
.
isoformat
()
if
latest_record
else
None
})
finally
:
session
.
close
()
except
Exception
as
e
:
logger
.
error
(
f
"API get redistribution balance error: {e}"
)
return
jsonify
({
"error"
:
str
(
e
)}),
500
@
api_bp
.
route
(
'/redistribution-balance/reset'
,
methods
=
[
'POST'
])
@
get_api_auth_decorator
()
@
get_api_auth_decorator
(
require_admin
=
True
)
def
reset_redistribution_balance
():
"""Reset redistribution balance to zero (admin only)"""
try
:
from
..database.models
import
PersistentRedistributionAdjustmentModel
from
datetime
import
date
,
datetime
session
=
api_bp
.
db_manager
.
get_session
()
try
:
# Get the latest redistribution adjustment record
latest_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
\
.
order_by
(
PersistentRedistributionAdjustmentModel
.
date
.
desc
())
\
.
first
()
if
latest_record
:
# Reset the accumulated shortfall to zero
old_balance
=
float
(
latest_record
.
accumulated_shortfall
)
latest_record
.
accumulated_shortfall
=
0.0
latest_record
.
updated_at
=
datetime
.
utcnow
()
session
.
commit
()
logger
.
info
(
f
"Redistribution balance reset from {old_balance} to 0.0"
)
return
jsonify
({
"success"
:
True
,
"message"
:
f
"Redistribution balance reset from {old_balance:.2f} to 0.00"
,
"old_balance"
:
old_balance
,
"new_balance"
:
0.0
})
else
:
# No record exists, create one with zero balance
today
=
date
.
today
()
new_record
=
PersistentRedistributionAdjustmentModel
(
date
=
today
,
accumulated_shortfall
=
0.0
,
cap_percentage
=
70.0
# Default cap
)
session
.
add
(
new_record
)
session
.
commit
()
logger
.
info
(
"Created new redistribution adjustment record with zero balance"
)
return
jsonify
({
"success"
:
True
,
"message"
:
"Redistribution balance set to 0.00 (new record created)"
,
"old_balance"
:
None
,
"new_balance"
:
0.0
})
finally
:
session
.
close
()
except
Exception
as
e
:
logger
.
error
(
f
"API reset redistribution balance error: {e}"
)
return
jsonify
({
"error"
:
str
(
e
)}),
500
# Currency Settings API routes
@
api_bp
.
route
(
'/currency-settings'
)
@
get_api_auth_decorator
()
...
...
mbetterclient/web_dashboard/templates/dashboard/index.html
View file @
a1afa146
...
...
@@ -122,6 +122,42 @@
</div>
</div>
{% endif %}
<!-- Redistribution Balance Management (Admin Only) -->
{% if current_user.is_admin %}
<div
class=
"row mt-3 pt-3 border-top"
>
<div
class=
"col-12"
>
<h6
class=
"text-muted mb-3"
>
<i
class=
"fas fa-balance-scale me-2"
></i>
Redistribution Balance
</h6>
</div>
<div
class=
"col-md-6 mb-3"
>
<div
class=
"card bg-light"
>
<div
class=
"card-body text-center p-3"
>
<h5
class=
"card-title mb-2"
>
<i
class=
"fas fa-coins me-2"
></i>
Current Balance
</h5>
<div
class=
"h4 mb-2 text-primary"
id=
"redistribution-balance"
>
--
</div>
<div
class=
"mt-2"
>
<small
class=
"text-muted"
>
<i
class=
"fas fa-info-circle me-1"
></i>
Balance to compensate over/under redistribution
</small>
</div>
</div>
</div>
</div>
<div
class=
"col-md-6 mb-3"
>
<div
class=
"d-grid"
>
<button
class=
"btn btn-outline-danger w-100"
id=
"btn-reset-redistribution-balance"
>
<i
class=
"fas fa-undo me-2"
></i>
Reset Balance to Zero
</button>
<small
class=
"text-muted mt-2 d-block"
>
<i
class=
"fas fa-exclamation-triangle me-1"
></i>
This will reset the redistribution compensation balance
</small>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
...
...
@@ -459,6 +495,11 @@ document.addEventListener('DOMContentLoaded', function() {
// Load license text configuration
loadLicenseText
();
// Load redistribution balance (admin only)
if
(
document
.
getElementById
(
'redistribution-balance'
))
{
loadRedistributionBalance
();
}
// Quick action buttons
document
.
getElementById
(
'btn-play-video'
).
addEventListener
(
'click'
,
function
()
{
...
...
@@ -499,6 +540,14 @@ document.addEventListener('DOMContentLoaded', function() {
document
.
getElementById
(
'btn-reset-license'
).
addEventListener
(
'click'
,
function
()
{
resetLicenseText
();
});
// Redistribution balance reset button (only exists for admin)
const
resetBalanceBtn
=
document
.
getElementById
(
'btn-reset-redistribution-balance'
);
if
(
resetBalanceBtn
)
{
resetBalanceBtn
.
addEventListener
(
'click'
,
function
()
{
resetRedistributionBalance
();
});
}
// Admin shutdown button (only exists if user is admin)
const
shutdownBtn
=
document
.
getElementById
(
'btn-shutdown-app'
);
...
...
@@ -1012,5 +1061,70 @@ function resetLicenseText() {
document
.
getElementById
(
'license-text'
).
value
=
defaultText
;
}
function
loadRedistributionBalance
()
{
fetch
(
'/api/redistribution-balance'
)
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
const
balanceElement
=
document
.
getElementById
(
'redistribution-balance'
);
const
balance
=
data
.
redistribution_balance
;
balanceElement
.
textContent
=
balance
.
toFixed
(
2
);
// Color coding: green for positive, red for negative, neutral for zero
if
(
balance
>
0
)
{
balanceElement
.
className
=
'h4 mb-2 text-success'
;
}
else
if
(
balance
<
0
)
{
balanceElement
.
className
=
'h4 mb-2 text-danger'
;
}
else
{
balanceElement
.
className
=
'h4 mb-2 text-primary'
;
}
}
else
{
document
.
getElementById
(
'redistribution-balance'
).
textContent
=
'Error'
;
console
.
error
(
'Failed to load redistribution balance:'
,
data
.
error
);
}
})
.
catch
(
error
=>
{
document
.
getElementById
(
'redistribution-balance'
).
textContent
=
'Error'
;
console
.
error
(
'Error loading redistribution balance:'
,
error
);
});
}
function
resetRedistributionBalance
()
{
if
(
!
confirm
(
'Are you sure you want to reset the redistribution balance to zero? This action cannot be undone.'
))
{
return
;
}
const
resetBtn
=
document
.
getElementById
(
'btn-reset-redistribution-balance'
);
const
originalText
=
resetBtn
.
innerHTML
;
// Show loading state
resetBtn
.
disabled
=
true
;
resetBtn
.
innerHTML
=
'<i class="fas fa-spinner fa-spin me-2"></i>Resetting...'
;
fetch
(
'/api/redistribution-balance/reset'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
}
})
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
alert
(
data
.
message
);
// Reload the balance
loadRedistributionBalance
();
}
else
{
alert
(
'Failed to reset redistribution balance: '
+
(
data
.
error
||
'Unknown error'
));
}
})
.
catch
(
error
=>
{
alert
(
'Error resetting redistribution balance: '
+
error
.
message
);
})
.
finally
(()
=>
{
// Restore button state
resetBtn
.
disabled
=
false
;
resetBtn
.
innerHTML
=
originalText
;
});
}
</script>
{% endblock %}
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