Add reports sync last sync query endpoint and client implementation guides

- Add GET /api/reports/last-sync endpoint to query server for last sync information
- Update reports page with sorting by client name and cap balance
- Create comprehensive client-side implementation guide (CLIENT_SYNC_IMPLEMENTATION_GUIDE.md)
- Create minimal client prompt (CLIENT_SYNC_MINIMAL_PROMPT.md)
- Update final implementation documentation (REPORTS_FINAL_IMPLEMENTATION.md)

This allows clients to:
- Query server for last sync information before syncing
- Verify local tracking against server state
- Recover from tracking corruption
- Prevent data loss from missed syncs
parent 9158dbaf
This diff is collapsed.
# Minimal Prompt: Client-Side Last Sync Query Implementation
## What Changed on Server
Server now has a new endpoint to query last sync information:
**Endpoint**: `GET /api/reports/last-sync?client_id=<client_id>`
**Authentication**: Bearer token (API token)
**Response Format**:
```json
{
"success": true,
"client_id": "client_unique_identifier",
"last_sync_id": "sync_20260201_214327_abc12345",
"last_sync_timestamp": "2026-02-01T21:43:27.249Z",
"last_sync_type": "incremental",
"total_syncs": 25,
"last_sync_summary": {
"total_payin": 100000.0,
"total_payout": 95000.0,
"net_profit": 5000.0,
"total_bets": 50,
"total_matches": 10,
"cap_compensation_balance": 5000.0
},
"server_timestamp": "2026-02-01T21:43:27.249Z"
}
```
## What You Need to Implement
### 1. Add Function to Query Server
```python
def query_server_last_sync(api_token, client_id):
"""Query server for last sync information"""
import requests
url = "https://your-server.com/api/reports/last-sync"
headers = {"Authorization": f"Bearer {api_token}"}
params = {"client_id": client_id}
response = requests.get(url, headers=headers, params=params)
return response.json()
```
### 2. Call Before Each Sync
```python
# Before performing sync
server_info = query_server_last_sync(api_token, client_id)
if server_info.get('success'):
last_sync_id = server_info.get('last_sync_id')
last_sync_time = server_info.get('last_sync_timestamp')
# Compare with your local tracking
# If mismatch detected, perform full sync instead of incremental
```
### 3. Handle Recovery
If your local tracking is corrupted or lost:
```python
# If no local tracking exists
if not local_tracking_exists():
# Query server for last sync
server_info = query_server_last_sync(api_token, client_id)
# Recover local tracking from server state
if server_info.get('last_sync_id'):
update_local_tracking(
sync_id=server_info['last_sync_id'],
timestamp=server_info['last_sync_timestamp']
)
```
## Key Benefits
1. **Verify Server State**: Check what server has before syncing
2. **Detect Corruption**: Compare local tracking with server
3. **Auto-Recovery**: Restore local tracking from server if lost
4. **Prevent Data Loss**: Ensure no syncs are missed
## Integration Point
Add this call to your existing sync flow:
```python
# Existing sync flow
def perform_sync():
# NEW: Query server first
server_info = query_server_last_sync(api_token, client_id)
# Verify and recover if needed
if needs_recovery(server_info):
recover_from_server(server_info)
# Continue with normal sync
send_sync_data()
```
That's it! Just add the query call before your existing sync logic.
\ No newline at end of file
This diff is collapsed.
...@@ -830,6 +830,193 @@ class Migration_011_AddCapCompensationBalance(Migration): ...@@ -830,6 +830,193 @@ class Migration_011_AddCapCompensationBalance(Migration):
def can_rollback(self) -> bool: def can_rollback(self) -> bool:
return True return True
class Migration_012_AddMatchNumberToBetsAndStats(Migration):
"""Add match_id and match_number to bets table, match_number to extraction_stats table"""
def __init__(self):
super().__init__("012", "Add match_id and match_number to bets table, match_number to extraction_stats table")
def up(self):
"""Add match_id and match_number columns to bets and extraction_stats tables"""
try:
inspector = inspect(db.engine)
# Check if bets table exists
if 'bets' not in inspector.get_table_names():
logger.info("bets table does not exist yet, skipping migration")
return True
# Add match_id and match_number to bets table
bets_columns = [col['name'] for col in inspector.get_columns('bets')]
if 'match_id' not in bets_columns:
logger.info("Adding match_id column to bets table...")
alter_table_sql = '''
ALTER TABLE bets
ADD COLUMN match_id INT NOT NULL DEFAULT 0
'''
with db.engine.connect() as conn:
conn.execute(text(alter_table_sql))
conn.commit()
logger.info("match_id column added to bets table")
else:
logger.info("match_id column already exists in bets table")
if 'match_number' not in bets_columns:
logger.info("Adding match_number column to bets table...")
alter_table_sql = '''
ALTER TABLE bets
ADD COLUMN match_number INT NOT NULL DEFAULT 0
'''
with db.engine.connect() as conn:
conn.execute(text(alter_table_sql))
conn.commit()
logger.info("match_number column added to bets table")
else:
logger.info("match_number column already exists in bets table")
# Create index on bets.match_id for better query performance
try:
with db.engine.connect() as conn:
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_bets_match_id ON bets(match_id)"))
conn.commit()
logger.info("Index idx_bets_match_id created")
except Exception as e:
logger.warning(f"Index idx_bets_match_id already exists or error: {e}")
# Check if extraction_stats table exists
if 'extraction_stats' not in inspector.get_table_names():
logger.info("extraction_stats table does not exist yet, skipping migration")
return True
# Add match_number to extraction_stats table
extraction_stats_columns = [col['name'] for col in inspector.get_columns('extraction_stats')]
if 'match_number' not in extraction_stats_columns:
logger.info("Adding match_number column to extraction_stats table...")
alter_table_sql = '''
ALTER TABLE extraction_stats
ADD COLUMN match_number INT NOT NULL DEFAULT 0
'''
with db.engine.connect() as conn:
conn.execute(text(alter_table_sql))
conn.commit()
logger.info("match_number column added to extraction_stats table")
else:
logger.info("match_number column already exists in extraction_stats table")
logger.info("Migration 012 completed successfully")
return True
except Exception as e:
logger.error(f"Migration 012 failed: {str(e)}")
raise
def down(self):
"""Drop match_id and match_number columns from bets and extraction_stats tables"""
try:
with db.engine.connect() as conn:
# Drop columns from bets table
conn.execute(text("DROP INDEX IF EXISTS idx_bets_match_id"))
conn.execute(text("ALTER TABLE bets DROP COLUMN IF EXISTS match_id"))
conn.execute(text("ALTER TABLE bets DROP COLUMN IF EXISTS match_number"))
# Drop column from extraction_stats table
conn.execute(text("ALTER TABLE extraction_stats DROP COLUMN IF EXISTS match_number"))
conn.commit()
logger.info("Dropped match_id and match_number columns from bets and extraction_stats tables")
return True
except Exception as e:
logger.error(f"Rollback of migration 012 failed: {str(e)}")
raise
def can_rollback(self) -> bool:
return True
class Migration_013_CreateMatchReportsTable(Migration):
"""Create match_reports table for comprehensive match-level reporting"""
def __init__(self):
super().__init__("013", "Create match_reports table for comprehensive match-level reporting")
def up(self):
"""Create match_reports table"""
try:
inspector = inspect(db.engine)
# Check if table already exists
if 'match_reports' in inspector.get_table_names():
logger.info("match_reports table already exists, skipping creation")
return True
# Create the table using raw SQL to ensure compatibility
create_table_sql = '''
CREATE TABLE match_reports (
id INT AUTO_INCREMENT PRIMARY KEY,
sync_id INT NOT NULL,
client_id VARCHAR(255) NOT NULL,
client_token_name VARCHAR(255) NOT NULL,
match_id INT NOT NULL,
match_number INT NOT NULL,
fixture_id VARCHAR(255) NOT NULL,
match_datetime DATETIME NOT NULL,
total_bets INT NOT NULL DEFAULT 0,
winning_bets INT NOT NULL DEFAULT 0,
losing_bets INT NOT NULL DEFAULT 0,
pending_bets INT NOT NULL DEFAULT 0,
total_payin DECIMAL(15,2) NOT NULL DEFAULT 0.00,
total_payout DECIMAL(15,2) NOT NULL DEFAULT 0.00,
balance DECIMAL(15,2) NOT NULL DEFAULT 0.00,
actual_result VARCHAR(50) NOT NULL,
extraction_result VARCHAR(50) NOT NULL,
cap_applied BOOLEAN NOT NULL DEFAULT FALSE,
cap_percentage DECIMAL(5,2),
cap_compensation_balance DECIMAL(15,2) NOT NULL DEFAULT 0.00,
under_bets INT NOT NULL DEFAULT 0,
under_amount DECIMAL(15,2) NOT NULL DEFAULT 0.00,
over_bets INT NOT NULL DEFAULT 0,
over_amount DECIMAL(15,2) NOT NULL DEFAULT 0.00,
result_breakdown JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_match_reports_sync_id (sync_id),
INDEX idx_match_reports_client_id (client_id),
INDEX idx_match_reports_client_token_name (client_token_name),
INDEX idx_match_reports_match_id (match_id),
INDEX idx_match_reports_fixture_id (fixture_id),
INDEX idx_match_reports_match_datetime (match_datetime),
INDEX idx_match_reports_actual_result (actual_result),
FOREIGN KEY (sync_id) REFERENCES report_syncs(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
'''
with db.engine.connect() as conn:
conn.execute(text(create_table_sql))
conn.commit()
logger.info("Created match_reports table successfully")
return True
except Exception as e:
logger.error(f"Migration 013 failed: {str(e)}")
raise
def down(self):
"""Drop match_reports table"""
try:
with db.engine.connect() as conn:
conn.execute(text("DROP TABLE IF EXISTS match_reports"))
conn.commit()
logger.info("Dropped match_reports table")
return True
except Exception as e:
logger.error(f"Rollback of migration 013 failed: {str(e)}")
raise
def can_rollback(self) -> bool:
return True
class MigrationManager: class MigrationManager:
"""Manages database migrations and versioning""" """Manages database migrations and versioning"""
...@@ -846,6 +1033,8 @@ class MigrationManager: ...@@ -846,6 +1033,8 @@ class MigrationManager:
Migration_009_CreateClientActivityTable(), Migration_009_CreateClientActivityTable(),
Migration_010_CreateReportsTables(), Migration_010_CreateReportsTables(),
Migration_011_AddCapCompensationBalance(), Migration_011_AddCapCompensationBalance(),
Migration_012_AddMatchNumberToBetsAndStats(),
Migration_013_CreateMatchReportsTable(),
] ]
def ensure_version_table(self): def ensure_version_table(self):
......
"""
Migration script to add match_id and match_number fields to bets and extraction_stats tables
"""
def upgrade():
"""Add match_id and match_number fields to bets table, match_number to extraction_stats table"""
from app import create_app, db
from app.models import Bet, ExtractionStats
app = create_app()
with app.app_context():
# Check if match_id column exists in bets table
inspector = db.inspect(db.engine)
bets_columns = [col['name'] for col in inspector.get_columns('bets')]
if 'match_id' not in bets_columns:
print("Adding match_id column to bets table...")
db.engine.execute(db.DDL(
"ALTER TABLE bets ADD COLUMN match_id INTEGER NOT NULL DEFAULT 0"
))
print("match_id column added to bets table")
else:
print("match_id column already exists in bets table")
if 'match_number' not in bets_columns:
print("Adding match_number column to bets table...")
db.engine.execute(db.DDL(
"ALTER TABLE bets ADD COLUMN match_number INTEGER NOT NULL DEFAULT 0"
))
print("match_number column added to bets table")
else:
print("match_number column already exists in bets table")
# Check if match_number column exists in extraction_stats table
extraction_stats_columns = [col['name'] for col in inspector.get_columns('extraction_stats')]
if 'match_number' not in extraction_stats_columns:
print("Adding match_number column to extraction_stats table...")
db.engine.execute(db.DDL(
"ALTER TABLE extraction_stats ADD COLUMN match_number INTEGER NOT NULL DEFAULT 0"
))
print("match_number column added to extraction_stats table")
else:
print("match_number column already exists in extraction_stats table")
# Create indexes for better query performance
print("Creating indexes...")
# Index on bets.match_id
try:
db.engine.execute(db.DDL(
"CREATE INDEX IF NOT EXISTS idx_bets_match_id ON bets(match_id)"
))
print("Index idx_bets_match_id created")
except Exception as e:
print(f"Index idx_bets_match_id already exists or error: {e}")
print("\nMigration completed successfully!")
def downgrade():
"""Remove match_id and match_number fields from bets and extraction_stats tables"""
from app import create_app, db
app = create_app()
with app.app_context():
print("Removing match_id and match_number columns from bets table...")
db.engine.execute(db.DDL("ALTER TABLE bets DROP COLUMN match_id"))
db.engine.execute(db.DDL("ALTER TABLE bets DROP COLUMN match_number"))
print("Removing match_number column from extraction_stats table...")
db.engine.execute(db.DDL("ALTER TABLE extraction_stats DROP COLUMN match_number"))
print("\nDowngrade completed successfully!")
if __name__ == '__main__':
import sys
if len(sys.argv) > 1 and sys.argv[1] == 'downgrade':
downgrade()
else:
upgrade()
\ No newline at end of file
...@@ -1742,6 +1742,10 @@ def reports(): ...@@ -1742,6 +1742,10 @@ def reports():
clients_list.sort(key=lambda x: x['winning_bets'], reverse=(sort_order == 'desc')) clients_list.sort(key=lambda x: x['winning_bets'], reverse=(sort_order == 'desc'))
elif sort_by == 'losing_bets': elif sort_by == 'losing_bets':
clients_list.sort(key=lambda x: x['losing_bets'], reverse=(sort_order == 'desc')) clients_list.sort(key=lambda x: x['losing_bets'], reverse=(sort_order == 'desc'))
elif sort_by == 'token_name':
clients_list.sort(key=lambda x: x['token_name'].lower(), reverse=(sort_order == 'desc'))
elif sort_by == 'cap_balance':
clients_list.sort(key=lambda x: x['cap_balance'], reverse=(sort_order == 'desc'))
else: else:
clients_list.sort(key=lambda x: x['last_match_timestamp'], reverse=(sort_order == 'desc')) clients_list.sort(key=lambda x: x['last_match_timestamp'], reverse=(sort_order == 'desc'))
......
...@@ -898,6 +898,8 @@ class Bet(db.Model): ...@@ -898,6 +898,8 @@ class Bet(db.Model):
sync_id = db.Column(db.Integer, db.ForeignKey('report_syncs.id'), nullable=False, index=True) sync_id = db.Column(db.Integer, db.ForeignKey('report_syncs.id'), nullable=False, index=True)
client_id = db.Column(db.String(255), nullable=False, index=True) client_id = db.Column(db.String(255), nullable=False, index=True)
fixture_id = db.Column(db.String(255), nullable=False, index=True) fixture_id = db.Column(db.String(255), nullable=False, index=True)
match_id = db.Column(db.Integer, nullable=False, index=True)
match_number = db.Column(db.Integer, nullable=False)
bet_datetime = db.Column(db.DateTime, nullable=False, index=True) bet_datetime = db.Column(db.DateTime, nullable=False, index=True)
paid = db.Column(db.Boolean, default=False) paid = db.Column(db.Boolean, default=False)
paid_out = db.Column(db.Boolean, default=False) paid_out = db.Column(db.Boolean, default=False)
...@@ -918,6 +920,8 @@ class Bet(db.Model): ...@@ -918,6 +920,8 @@ class Bet(db.Model):
'sync_id': self.sync_id, 'sync_id': self.sync_id,
'client_id': self.client_id, 'client_id': self.client_id,
'fixture_id': self.fixture_id, 'fixture_id': self.fixture_id,
'match_id': self.match_id,
'match_number': self.match_number,
'bet_datetime': self.bet_datetime.isoformat() if self.bet_datetime else None, 'bet_datetime': self.bet_datetime.isoformat() if self.bet_datetime else None,
'paid': self.paid, 'paid': self.paid,
'paid_out': self.paid_out, 'paid_out': self.paid_out,
...@@ -970,6 +974,7 @@ class ExtractionStats(db.Model): ...@@ -970,6 +974,7 @@ class ExtractionStats(db.Model):
sync_id = db.Column(db.Integer, db.ForeignKey('report_syncs.id'), nullable=False, index=True) sync_id = db.Column(db.Integer, db.ForeignKey('report_syncs.id'), nullable=False, index=True)
client_id = db.Column(db.String(255), nullable=False, index=True) client_id = db.Column(db.String(255), nullable=False, index=True)
match_id = db.Column(db.Integer, nullable=False, index=True) match_id = db.Column(db.Integer, nullable=False, index=True)
match_number = db.Column(db.Integer, nullable=False)
fixture_id = db.Column(db.String(255), nullable=False, index=True) fixture_id = db.Column(db.String(255), nullable=False, index=True)
match_datetime = db.Column(db.DateTime, nullable=False) match_datetime = db.Column(db.DateTime, nullable=False)
total_bets = db.Column(db.Integer, nullable=False) total_bets = db.Column(db.Integer, nullable=False)
...@@ -995,6 +1000,7 @@ class ExtractionStats(db.Model): ...@@ -995,6 +1000,7 @@ class ExtractionStats(db.Model):
'sync_id': self.sync_id, 'sync_id': self.sync_id,
'client_id': self.client_id, 'client_id': self.client_id,
'match_id': self.match_id, 'match_id': self.match_id,
'match_number': self.match_number,
'fixture_id': self.fixture_id, 'fixture_id': self.fixture_id,
'match_datetime': self.match_datetime.isoformat() if self.match_datetime else None, 'match_datetime': self.match_datetime.isoformat() if self.match_datetime else None,
'total_bets': self.total_bets, 'total_bets': self.total_bets,
......
...@@ -104,11 +104,13 @@ ...@@ -104,11 +104,13 @@
<label for="sort_by" class="form-label">Sort By</label> <label for="sort_by" class="form-label">Sort By</label>
<select class="form-select" id="sort_by" name="sort_by"> <select class="form-select" id="sort_by" name="sort_by">
<option value="last_match_timestamp" {% if filters.sort_by == 'last_match_timestamp' %}selected{% endif %}>Last Match</option> <option value="last_match_timestamp" {% if filters.sort_by == 'last_match_timestamp' %}selected{% endif %}>Last Match</option>
<option value="token_name" {% if filters.sort_by == 'token_name' %}selected{% endif %}>Client Name</option>
<option value="total_payin" {% if filters.sort_by == 'total_payin' %}selected{% endif %}>Total Payin</option> <option value="total_payin" {% if filters.sort_by == 'total_payin' %}selected{% endif %}>Total Payin</option>
<option value="total_payout" {% if filters.sort_by == 'total_payout' %}selected{% endif %}>Total Payout</option> <option value="total_payout" {% if filters.sort_by == 'total_payout' %}selected{% endif %}>Total Payout</option>
<option value="net_profit" {% if filters.sort_by == 'net_profit' %}selected{% endif %}>Net Profit</option> <option value="net_profit" {% if filters.sort_by == 'net_profit' %}selected{% endif %}>Net Profit</option>
<option value="total_bets" {% if filters.sort_by == 'total_bets' %}selected{% endif %}>Total Bets</option> <option value="total_bets" {% if filters.sort_by == 'total_bets' %}selected{% endif %}>Total Bets</option>
<option value="total_matches" {% if filters.sort_by == 'total_matches' %}selected{% endif %}>Total Matches</option> <option value="total_matches" {% if filters.sort_by == 'total_matches' %}selected{% endif %}>Total Matches</option>
<option value="cap_balance" {% if filters.sort_by == 'cap_balance' %}selected{% endif %}>CAP Balance</option>
</select> </select>
</div> </div>
<div class="col-12"> <div class="col-12">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment