Add deadline and refactor squad pages

This commit is contained in:
Jonny Ervine 2025-10-22 21:57:18 +08:00
parent c199979eb9
commit 0cf8cf7fc0
25 changed files with 1651 additions and 188 deletions

View File

@ -0,0 +1,209 @@
# Debugging URL Suffix Not Saving Issue
## Problem
The app is not saving the URL suffix to the `admin_settings` table.
## Debug Logging Added
I've added comprehensive debug logging to help diagnose the issue. The logs will show:
1. **When the form is submitted:**
- Which button was clicked (Save vs Activate)
- What form data was received
2. **When saving settings:**
- The generated URL suffix
- Whether the UPDATE query executed
- The result of the UPDATE query
- Verification of what's actually in the database
3. **When activating voting:**
- What URL suffix is retrieved from the database
- Whether a new suffix needs to be generated
- The final suffix being used
## How to Debug
### Step 1: Deploy the Updated Code
Deploy the updated code with debug logging to production:
```bash
# Build new image
docker build -t your-registry/motm-app:latest .
# Push to registry
docker push your-registry/motm-app:latest
# Deploy to Kubernetes
helm upgrade motm-app ./helm-chart/motm-app --namespace motm-app
```
### Step 2: Test the MOTM Admin Page
1. Go to https://motm.ervine.cloud/admin/motm
2. Fill in the form:
- Match date
- Opposition team
- Current MOTM (optional)
- Current DotD (optional)
- Voting deadline (optional)
3. Click **"Save Settings"** button
4. Check the logs
### Step 3: Check the Logs
```bash
# Get the pod name
kubectl get pods -n motm-app -l app.kubernetes.io/name=motm-app
# Watch the logs in real-time
kubectl logs -n motm-app -f <POD_NAME>
```
Or check recent logs:
```bash
kubectl logs -n motm-app -l app.kubernetes.io/name=motm-app --tail=100 | grep DEBUG
```
### Step 4: Analyze the Debug Output
Look for these debug messages:
#### When clicking "Save Settings":
```
DEBUG: POST request received
DEBUG: form.saveButton.data = True
DEBUG: form.activateButton.data = False
DEBUG: Save button clicked
DEBUG: Form data - team: [team name], date: [date]
DEBUG: Generated URL suffix: [suffix]
DEBUG: About to execute UPDATE query
DEBUG: UPDATE query result: True
DEBUG: Verification - URL suffix in DB: [suffix]
```
#### When clicking "Activate MotM Vote":
```
DEBUG: POST request received
DEBUG: form.saveButton.data = False
DEBUG: form.activateButton.data = True
DEBUG: Activate button clicked
DEBUG: Form data - team: [team name], date: [date]
DEBUG: Getting URL suffix from database
DEBUG: Query result: [result]
DEBUG: Using existing suffix: [suffix]
DEBUG: Final suffix: [suffix]
```
## Common Issues and Solutions
### Issue 1: Button Click Not Detected
**Symptoms:**
```
DEBUG: POST request received
DEBUG: form.saveButton.data = False
DEBUG: form.activateButton.data = False
DEBUG: Something went wrong - check with Smithers
```
**Cause:** The form button data isn't being submitted correctly.
**Solution:** Check the HTML form to ensure the buttons have the correct `name` attribute.
### Issue 2: UPDATE Query Fails
**Symptoms:**
```
DEBUG: UPDATE query result: False
```
**Cause:** The UPDATE query failed silently.
**Solution:** Check for SQL errors in the logs. The `sql_write_static` function prints errors.
### Issue 3: UPDATE Succeeds but Value Not Saved
**Symptoms:**
```
DEBUG: UPDATE query result: True
DEBUG: Verification - URL suffix in DB: None
```
**Cause:** The UPDATE query executed but didn't actually update the row.
**Solution:**
- Check if the WHERE clause is matching a row
- Verify the table structure
- Check if there's a transaction rollback happening
### Issue 4: Query Returns Empty Result
**Symptoms:**
```
DEBUG: Query result: []
```
**Cause:** No row matches the WHERE clause `userid = 'admin'`.
**Solution:**
- Check if the `userid` column has the value 'admin'
- Run the diagnostic script: `check_production_db.py`
## Manual Verification
After clicking "Save Settings", verify the URL suffix was saved:
```bash
kubectl exec -it <POD_NAME> -n motm-app -- python -c "
from db_config import sql_read_static
from sqlalchemy import text
result = sql_read_static(text('SELECT motm_url_suffix FROM admin_settings WHERE userid = \\'admin\\''))
print('URL suffix in database:', result[0]['motm_url_suffix'] if result else 'No data')
"
```
## Expected Behavior
### When clicking "Save Settings":
1. Form data is received ✅
2. URL suffix is generated ✅
3. UPDATE query executes ✅
4. URL suffix is saved to database ✅
5. Flash message shows the URL ✅
### When clicking "Activate MotM Vote":
1. Form data is received ✅
2. URL suffix is retrieved from database ✅
3. If missing, a new one is generated and saved ✅
4. Flash message shows the URL ✅
## Next Steps
1. **Deploy the updated code** with debug logging
2. **Test the MOTM admin page** and click "Save Settings"
3. **Check the logs** for debug messages
4. **Share the debug output** if the issue persists
5. **Verify the database** using the manual verification command
## Removing Debug Logging
Once the issue is resolved, you can remove the debug logging by searching for lines containing:
```python
print(f"DEBUG:
```
And removing them, or I can create a cleaned-up version for you.
## Support
If you continue to have issues after checking the debug logs:
1. Save the complete debug output
2. Check the application logs for any errors
3. Verify the database connection is working
4. Check if there are any database permission issues
5. Share the debug output and any error messages

View File

@ -0,0 +1,107 @@
# Database Migration Summary
## Problem
Your production application is throwing SQL errors because:
1. The `votingdeadline` column is missing from the `admin_settings` table
2. The code was using wrong table/column names (fixed in code)
## What Was Fixed
### Code Changes (Already Applied)
✅ Fixed table name: `motmadminsettings``admin_settings`
✅ Fixed column names to use snake_case (e.g., `nextclub``next_club`)
✅ Added camelCase to snake_case conversion in `readSettings.py`
✅ Improved error handling for duplicate column creation
### Files Modified
- `main.py` - Updated all SQL queries
- `readSettings.py` - Added camelCase to snake_case conversion
## What You Need to Do
### Run the Migration on Production
**Option 1: Use the automated script (Easiest)**
```bash
cd /home/jonny/Projects/gcp-hockey-results/motm_app
./run_production_migration.sh motm-app
```
**Option 2: Manual steps**
```bash
# 1. Find your pod
kubectl get pods -n motm-app -l app.kubernetes.io/name=motm-app
# 2. Run the migration
kubectl exec -it <POD_NAME> -n motm-app -- python add_voting_deadline.py
# 3. Verify it worked
kubectl exec -it <POD_NAME> -n motm-app -- python -c "
from db_config import db_config
from sqlalchemy import text, inspect
engine = db_config.engine
inspector = inspect(engine)
columns = inspector.get_columns('admin_settings')
print('✓ votingdeadline exists' if any(c['name'] == 'votingdeadline' for c in columns) else '✗ missing')
"
# 4. Restart the pod
kubectl rollout restart deployment/motm-app -n motm-app
```
## Expected Results
After running the migration:
- ✅ The `votingdeadline` column will be added to `admin_settings` table
- ✅ The MOTM admin page will load without SQL errors
- ✅ You'll see the "Voting Deadline" field in the form
- ✅ You can set voting deadlines for matches
## Verification
Test the application:
1. Visit https://motm.ervine.cloud/admin/motm
2. Page should load without errors
3. You should see the "Voting Deadline" field
4. Set a deadline and activate voting
5. Visit the voting page - you should see a countdown timer
## Troubleshooting
### If the migration script isn't in the pod:
You need to rebuild and redeploy the Docker image:
```bash
# Build new image with migration script
docker build -t your-registry/motm-app:latest .
# Push to registry
docker push your-registry/motm-app:latest
# Deploy to Kubernetes
helm upgrade motm-app ./helm-chart/motm-app --namespace motm-app
```
### If you get "column already exists" error:
This is fine! The migration script is idempotent and will skip if the column already exists.
### If you get other errors:
Check the logs:
```bash
kubectl logs -n motm-app -l app.kubernetes.io/name=motm-app --tail=100
```
## Documentation
For detailed instructions, see:
- `PRODUCTION_MIGRATION_GUIDE.md` - Comprehensive migration guide
- `VOTING_DEADLINE_FEATURE.md` - Feature documentation
- `VOTING_DEADLINE_IMPLEMENTATION.md` - Implementation details
## Support
If you encounter issues:
1. Check pod logs for detailed error messages
2. Verify database connectivity
3. Ensure database user has ALTER TABLE permissions
4. Review the production migration guide

View File

@ -0,0 +1,234 @@
# Production Database Diagnostic Guide
## Issue
Getting "Admin settings not found" error when activating MOTM vote, even after saving settings.
## Root Cause Analysis
The code is looking for the `motm_url_suffix` column in the `admin_settings` table. The error suggests one of these issues in production:
1. **Wrong table name**: Production might be using `motmadminsettings` instead of `admin_settings`
2. **Missing column**: The `motm_url_suffix` column might not exist
3. **NULL value**: The `motm_url_suffix` might be NULL or empty
4. **No rows**: The table might be empty
## Diagnostic Steps
### Step 1: Run the Diagnostic Script
Run this in your production Kubernetes pod:
```bash
# Find your pod
kubectl get pods -n motm-app -l app.kubernetes.io/name=motm-app
# Run the diagnostic script
kubectl exec -it <POD_NAME> -n motm-app -- python check_production_db.py
```
### Step 2: Analyze the Output
The script will show:
- Which tables exist (`admin_settings` vs `motmadminsettings`)
- What data is in the table
- Whether the `motm_url_suffix` column has a value
- The exact query that's failing
### Step 3: Based on the Results
#### Scenario A: Table is `motmadminsettings` (Old Name)
If the output shows `motmadminsettings` exists but `admin_settings` doesn't:
**Solution**: Your production database still has the old table name. You need to either:
1. **Rename the table** (recommended):
```bash
kubectl exec -it <POD_NAME> -n motm-app -- python -c "
from db_config import db_config
from sqlalchemy import text
engine = db_config.engine
conn = engine.connect()
conn.execute(text('ALTER TABLE motmadminsettings RENAME TO admin_settings'))
conn.commit()
conn.close()
print('Table renamed successfully')
"
```
2. **Or update the code** to use `motmadminsettings` (not recommended, but works as a quick fix)
#### Scenario B: `motm_url_suffix` is NULL or Empty
If the output shows the column exists but is NULL or empty:
**Solution**: The column needs to be populated. Run this:
```bash
kubectl exec -it <POD_NAME> -n motm-app -- python -c "
from db_config import db_config
from sqlalchemy import text
import random
import string
# Generate a random URL suffix
def randomUrlSuffix(length=8):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
engine = db_config.engine
conn = engine.connect()
urlSuffix = randomUrlSuffix(8)
conn.execute(text('UPDATE admin_settings SET motm_url_suffix = :suffix WHERE userid = \\'admin\\''), {'suffix': urlSuffix})
conn.commit()
conn.close()
print(f'URL suffix set to: {urlSuffix}')
"
```
#### Scenario C: Table is Empty
If the output shows 0 rows:
**Solution**: You need to initialize the admin settings. Run this:
```bash
kubectl exec -it <POD_NAME> -n motm-app -- python -c "
from db_config import db_config
from sqlalchemy import text
import random
import string
def randomUrlSuffix(length=8):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
engine = db_config.engine
conn = engine.connect()
urlSuffix = randomUrlSuffix(8)
# Insert default admin settings
conn.execute(text('''
INSERT INTO admin_settings (userid, motm_url_suffix, next_fixture, prev_fixture)
VALUES ('admin', :suffix, 1, 0)
ON CONFLICT (userid) DO UPDATE SET motm_url_suffix = :suffix
'''), {'suffix': urlSuffix})
conn.commit()
conn.close()
print(f'Admin settings initialized with URL suffix: {urlSuffix}')
"
```
#### Scenario D: Column Doesn't Exist
If the output shows the column is missing:
**Solution**: Add the missing column:
```bash
kubectl exec -it <POD_NAME> -n motm-app -- python -c "
from db_config import db_config
from sqlalchemy import text
engine = db_config.engine
conn = engine.connect()
# Add the column if it doesn't exist
try:
conn.execute(text('ALTER TABLE admin_settings ADD COLUMN motm_url_suffix VARCHAR(50)'))
conn.commit()
print('Column added successfully')
except Exception as e:
if 'already exists' in str(e):
print('Column already exists')
else:
print(f'Error: {e}')
conn.close()
"
```
## Quick Fix Script
If you want to try a comprehensive fix that handles all scenarios:
```bash
kubectl exec -it <POD_NAME> -n motm-app -- python << 'EOF'
from db_config import db_config
from sqlalchemy import text
import random
import string
def randomUrlSuffix(length=8):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
engine = db_config.engine
conn = engine.connect()
# Step 1: Check if table exists
try:
result = conn.execute(text("SELECT 1 FROM admin_settings LIMIT 1"))
print("✓ admin_settings table exists")
except:
print("✗ admin_settings table does not exist")
print("Please check if you're using the old table name 'motmadminsettings'")
conn.close()
exit(1)
# Step 2: Check if motm_url_suffix column exists
try:
result = conn.execute(text("SELECT motm_url_suffix FROM admin_settings LIMIT 1"))
print("✓ motm_url_suffix column exists")
except:
print("✗ motm_url_suffix column does not exist, adding it...")
conn.execute(text("ALTER TABLE admin_settings ADD COLUMN motm_url_suffix VARCHAR(50)"))
conn.commit()
print("✓ Column added")
# Step 3: Check if URL suffix has a value
result = conn.execute(text("SELECT motm_url_suffix FROM admin_settings WHERE userid = 'admin'"))
row = result.fetchone()
if row and row[0]:
print(f"✓ URL suffix exists: {row[0]}")
else:
print("✗ URL suffix is NULL or empty, setting it...")
urlSuffix = randomUrlSuffix(8)
conn.execute(text("UPDATE admin_settings SET motm_url_suffix = :suffix WHERE userid = 'admin'"), {'suffix': urlSuffix})
conn.commit()
print(f"✓ URL suffix set to: {urlSuffix}")
conn.close()
print("\n✓ All checks passed!")
EOF
```
## After Running the Fix
1. **Restart the application** to clear any cached connections:
```bash
kubectl rollout restart deployment/motm-app -n motm-app
```
2. **Test the MOTM admin page**:
- Go to https://motm.ervine.cloud/admin/motm
- Fill in the match details
- Click "Activate MotM Vote"
- It should work without errors
## Prevention
To prevent this issue in the future:
1. **Run database migrations** before deploying new code
2. **Use the migration script** (`add_voting_deadline.py`) to ensure all columns exist
3. **Test in staging** before deploying to production
4. **Monitor logs** for SQL errors
## Support
If you continue to have issues after running these diagnostics:
1. Save the output from `check_production_db.py`
2. Check the application logs: `kubectl logs -n motm-app -l app.kubernetes.io/name=motm-app --tail=100`
3. Verify the database connection is working
4. Check if there are any database permission issues

View File

@ -0,0 +1,219 @@
# Production Database Migration Guide
## Issue
The production database is missing the `votingdeadline` column, causing SQL errors when accessing the MOTM admin page.
## Solution
Run the migration script on the production Kubernetes cluster to add the missing column.
## Step-by-Step Instructions
### Option 1: Run Migration via kubectl exec (Recommended)
1. **Find your production pod:**
```bash
kubectl get pods -n <your-namespace> -l app.kubernetes.io/name=motm-app
```
Replace `<your-namespace>` with your actual namespace (e.g., `motm-app`, `default`, etc.)
2. **Execute the migration script:**
```bash
kubectl exec -it <pod-name> -n <your-namespace> -- python add_voting_deadline.py
```
Example:
```bash
kubectl exec -it motm-app-7d8f9b4c5-xk2mn -n motm-app -- python add_voting_deadline.py
```
3. **Verify the migration:**
```bash
kubectl exec -it <pod-name> -n <your-namespace> -- python -c "
from db_config import db_config
from sqlalchemy import text, inspect
engine = db_config.engine
inspector = inspect(engine)
columns = inspector.get_columns('admin_settings')
voting_deadline_exists = any(col['name'] == 'votingdeadline' for col in columns)
print('✓ votingdeadline column exists' if voting_deadline_exists else '✗ votingdeadline column missing')
"
```
### Option 2: Run Migration via Helm Job (Alternative)
If you prefer to run the migration as a Kubernetes job:
1. **Create a migration job manifest:**
```yaml
# migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: motm-migration-voting-deadline
namespace: motm-app
spec:
template:
spec:
containers:
- name: migration
image: your-registry/motm-app:latest
command: ["python", "add_voting_deadline.py"]
envFrom:
- secretRef:
name: motm-app-secrets
- configMapRef:
name: motm-app-config
restartPolicy: Never
backoffLimit: 3
```
2. **Apply the job:**
```bash
kubectl apply -f migration-job.yaml
```
3. **Check job status:**
```bash
kubectl get jobs -n motm-app
kubectl logs -n motm-app job/motm-migration-voting-deadline
```
4. **Clean up after successful migration:**
```bash
kubectl delete job motm-migration-voting-deadline -n motm-app
```
### Option 3: Run Migration via Helm Hook (Advanced)
For automated migrations during deployments:
1. **Create a migration job template:**
```yaml
# templates/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "motm-app.fullname" . }}-migration
namespace: {{ .Release.Namespace }}
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
containers:
- name: migration
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["python", "add_voting_deadline.py"]
envFrom:
- secretRef:
name: {{ include "motm-app.fullname" . }}-secrets
restartPolicy: Never
backoffLimit: 3
```
2. **The migration will run automatically during helm upgrade**
## Verification
After running the migration, verify it worked:
1. **Check the application logs:**
```bash
kubectl logs -n <your-namespace> -l app.kubernetes.io/name=motm-app --tail=50
```
2. **Test the admin page:**
- Navigate to `https://motm.ervine.cloud/admin/motm`
- The page should load without SQL errors
- You should see the "Voting Deadline" field in the form
3. **Check for the column in the database:**
```bash
kubectl exec -it <pod-name> -n <your-namespace> -- python -c "
from db_config import db_config
from sqlalchemy import text, inspect
engine = db_config.engine
inspector = inspect(engine)
columns = inspector.get_columns('admin_settings')
print('Columns in admin_settings:')
for col in columns:
print(f' - {col[\"name\"]}: {col[\"type\"]}')
"
```
You should see `votingdeadline` in the list.
## Troubleshooting
### Issue: "pod not found"
**Solution:** Make sure you're using the correct namespace and pod name.
### Issue: "permission denied"
**Solution:** The migration script needs database write permissions. Ensure your database user has ALTER TABLE permissions.
### Issue: "column already exists"
**Solution:** This is fine! The migration script is idempotent and will skip if the column already exists.
### Issue: Migration fails
**Solution:** Check the pod logs for detailed error messages:
```bash
kubectl logs -n <your-namespace> <pod-name>
```
## Rollback (If Needed)
If the migration causes issues, you can rollback:
1. **Remove the column:**
```bash
kubectl exec -it <pod-name> -n <your-namespace> -- python -c "
from db_config import db_config
from sqlalchemy import text
engine = db_config.engine
with engine.connect() as conn:
conn.execute(text('ALTER TABLE admin_settings DROP COLUMN IF EXISTS votingdeadline'))
conn.commit()
print('Column removed')
"
```
2. **Restart the application:**
```bash
kubectl rollout restart deployment/motm-app -n <your-namespace>
```
## Quick Reference
```bash
# Find pod name
kubectl get pods -n motm-app -l app.kubernetes.io/name=motm-app
# Run migration
kubectl exec -it <POD_NAME> -n motm-app -- python add_voting_deadline.py
# Verify migration
kubectl exec -it <POD_NAME> -n motm-app -- python -c "from db_config import db_config; from sqlalchemy import text, inspect; engine = db_config.engine; inspector = inspect(engine); columns = inspector.get_columns('admin_settings'); print('✓ votingdeadline exists' if any(c['name'] == 'votingdeadline' for c in columns) else '✗ missing')"
# Check logs
kubectl logs -n motm-app -l app.kubernetes.io/name=motm-app --tail=100
```
## Next Steps
After the migration is complete:
1. ✅ Restart the application pods to clear any cached connections
2. ✅ Test the MOTM admin page
3. ✅ Set a voting deadline for the next match
4. ✅ Test the voting page to see the countdown timer
## Support
If you encounter any issues:
1. Check the pod logs for detailed error messages
2. Verify database connectivity
3. Ensure the database user has proper permissions
4. Review the application logs for any additional errors

128
motm_app/REVERT_SUMMARY.md Normal file
View File

@ -0,0 +1,128 @@
# Revert to motmadminsettings - Summary
## What Was Reverted
All changes have been reverted to use the original table and column names that match your production database.
### Table Name
- **Before (my changes):** `admin_settings`
- **After (reverted):** `motmadminsettings`
### Column Names (camelCase)
All column names reverted from snake_case to camelCase:
| Old (snake_case) | New (camelCase) |
|------------------|-----------------|
| `next_club` | `nextclub` ✅ |
| `next_team` | `nextteam` ✅ |
| `next_date` | `nextdate` ✅ |
| `oppo_logo` | `oppologo` ✅ |
| `hkfc_logo` | `hkfclogo` ✅ |
| `curr_motm` | `currmotm` ✅ |
| `curr_dotd` | `currdotd` ✅ |
| `next_fixture` | `nextfixture` ✅ |
| `motm_url_suffix` | `motmurlsuffix` ✅ |
| `prev_fixture` | `prevfixture` ✅ |
### WHERE Clauses
Removed all `WHERE userid = 'admin'` clauses from queries since the `motmadminsettings` table doesn't have a `userid` column.
## Files Modified
1. **`main.py`**
- All table references: `admin_settings``motmadminsettings`
- All column names: snake_case → camelCase
- Removed `WHERE userid = 'admin'` from all queries
2. **`readSettings.py`**
- Table reference: `admin_settings``motmadminsettings`
- Removed `WHERE userid = 'admin'` from query
- Removed camelCase to snake_case conversion logic
## Key Changes
### SQL Queries
**SELECT queries:**
```sql
-- Before (my changes)
SELECT next_club, next_team FROM admin_settings WHERE userid = 'admin'
-- After (reverted)
SELECT nextclub, nextteam FROM motmadminsettings
```
**UPDATE queries:**
```sql
-- Before (my changes)
UPDATE admin_settings SET motm_url_suffix = :suffix WHERE userid = 'admin'
-- After (reverted)
UPDATE motmadminsettings SET motmurlsuffix = :suffix
```
**INSERT queries:**
```sql
-- Before (my changes)
INSERT INTO admin_settings (userid, next_club, next_team, ...)
-- After (reverted)
INSERT INTO motmadminsettings (nextclub, nextteam, ...)
```
## What Still Works
✅ All the bug fixes I made still work:
- Debug logging for URL suffix issues
- Fallback logic for UPDATE queries
- Voting deadline feature
- Error handling improvements
## What's Different
❌ No longer using:
- `admin_settings` table (reverted to `motmadminsettings`)
- snake_case column names (reverted to camelCase)
- `WHERE userid = 'admin'` clauses (removed)
## Production Compatibility
✅ **Now compatible with production database:**
- Uses `motmadminsettings` table
- Uses camelCase column names
- No `userid` column references
## Testing
The code should now work with your production database without any migration needed.
### Quick Test
```bash
# Test locally (if you have motmadminsettings table)
python -c "
from db_config import sql_read_static
from sqlalchemy import text
result = sql_read_static(text('SELECT * FROM motmadminsettings'))
print('Table exists:', len(result) > 0)
"
# Deploy to production
docker build -t your-registry/motm-app:latest .
docker push your-registry/motm-app:latest
helm upgrade motm-app ./helm-chart/motm-app --namespace motm-app
```
## Next Steps
1. ✅ Code is reverted to use `motmadminsettings`
2. 🚀 Deploy to production
3. 🧪 Test MOTM admin page
4. ✅ URL suffix should now save correctly
## Why This Happened
I initially assumed you were using the newer `admin_settings` table with snake_case columns (which is what the SQLAlchemy ORM model defines). However, your production database still uses the legacy `motmadminsettings` table with camelCase columns.
The revert ensures the code matches your actual production database schema.

View File

@ -0,0 +1,109 @@
# SQL WHERE Clause Fixes
## Problem
When activating the MOTM vote, the application was throwing an error: "Database not initialized. Please go to Database Setup to initialize the database."
## Root Cause
Multiple SQL queries on the `admin_settings` table were missing `WHERE userid = 'admin'` clauses, causing:
1. UPDATE queries to update ALL rows instead of just the admin row
2. SELECT queries to return unexpected results
3. The application to think the database wasn't initialized
## Fixes Applied
### 1. UPDATE Queries Fixed
**Line 599** - Main settings update:
```sql
-- BEFORE:
UPDATE admin_settings SET next_date = :next_date, next_club = :next_club, ...
-- AFTER:
UPDATE admin_settings SET next_date = :next_date, next_club = :next_club, ... WHERE userid = 'admin'
```
**Line 613** - Opponent logo update:
```sql
-- BEFORE:
UPDATE admin_settings SET oppo_logo = :logo_url
-- AFTER:
UPDATE admin_settings SET oppo_logo = :logo_url WHERE userid = 'admin'
```
### 2. SELECT Queries Fixed
**Line 256** - MOTM vote page:
```sql
-- BEFORE:
SELECT next_club, next_team, next_date, ... FROM admin_settings
-- AFTER:
SELECT next_club, next_team, next_date, ... FROM admin_settings WHERE userid = 'admin'
```
**Line 348** - Match comments page:
```sql
-- BEFORE:
SELECT next_club, next_team, next_date, oppo_logo, hkfc_logo FROM admin_settings
-- AFTER:
SELECT next_club, next_team, next_date, oppo_logo, hkfc_logo FROM admin_settings WHERE userid = 'admin'
```
**Line 683** - Admin settings page:
```sql
-- BEFORE:
SELECT next_club, oppo_logo FROM admin_settings
-- AFTER:
SELECT next_club, oppo_logo FROM admin_settings WHERE userid = 'admin'
```
## Why This Matters
Without the WHERE clause:
- **UPDATE** queries would modify all rows in the table (even if there's only one row, this is bad practice)
- **SELECT** queries might return multiple rows when only one is expected
- The application logic assumes only one admin settings row exists
## Testing
After these fixes:
1. ✅ The MOTM admin page should save settings correctly
2. ✅ Activating the MOTM vote should work without errors
3. ✅ The voting page should load correctly
4. ✅ All admin settings queries will target only the admin row
## Deployment
To apply these fixes to production:
1. Commit the changes to your repository
2. Rebuild and redeploy the Docker image
3. Restart the application pods
```bash
# Build new image
docker build -t your-registry/motm-app:latest .
# Push to registry
docker push your-registry/motm-app:latest
# Deploy to Kubernetes
helm upgrade motm-app ./helm-chart/motm-app --namespace motm-app
```
## Related Issues
These fixes are related to:
- The voting deadline feature implementation
- The table name migration from `motmadminsettings` to `admin_settings`
- The column name migration to snake_case
## Summary
All SQL queries on the `admin_settings` table now properly filter by `userid = 'admin'`, ensuring:
- Data integrity
- Predictable query results
- Proper application functionality

81
motm_app/check_production_db.py Executable file
View File

@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
Diagnostic script to check production database tables and data.
Run this in your production Kubernetes pod to diagnose the issue.
"""
from db_config import db_config, sql_read_static
from sqlalchemy import text, inspect
def check_database():
"""Check database tables and data"""
print("=" * 60)
print("Production Database Diagnostic")
print("=" * 60)
print()
engine = db_config.engine
inspector = inspect(engine)
tables = inspector.get_table_names()
print("1. Checking tables...")
print(f" Total tables: {len(tables)}")
print(f" Has 'admin_settings': {'admin_settings' in tables}")
print(f" Has 'motmadminsettings': {'motmadminsettings' in tables}")
print()
# Check admin_settings table
if 'admin_settings' in tables:
print("2. Checking 'admin_settings' table...")
try:
result = sql_read_static(text("SELECT * FROM admin_settings"))
print(f" Rows: {len(result)}")
if result:
row = result[0]
print(f" Columns: {list(row.keys())}")
print(f" userid: {row.get('userid')}")
print(f" next_date: {row.get('next_date')}")
print(f" next_club: {row.get('next_club')}")
print(f" next_team: {row.get('next_team')}")
print(f" motm_url_suffix: {row.get('motm_url_suffix')}")
print(f" votingdeadline: {row.get('votingdeadline')}")
except Exception as e:
print(f" Error: {e}")
print()
# Check motmadminsettings table
if 'motmadminsettings' in tables:
print("3. Checking 'motmadminsettings' table...")
try:
result = sql_read_static(text("SELECT * FROM motmadminsettings"))
print(f" Rows: {len(result)}")
if result:
row = result[0]
print(f" Columns: {list(row.keys())}")
except Exception as e:
print(f" Error: {e}")
print()
# Test the specific query that's failing
print("4. Testing the failing query...")
try:
sql5 = text("SELECT motm_url_suffix FROM admin_settings WHERE userid = 'admin'")
tempSuffix = sql_read_static(sql5)
print(f" Query result: {tempSuffix}")
print(f" Result length: {len(tempSuffix) if tempSuffix else 0}")
if tempSuffix:
print(f" motm_url_suffix value: {tempSuffix[0].get('motm_url_suffix')}")
print(f" Is None or empty: {not tempSuffix[0].get('motm_url_suffix')}")
else:
print(" ERROR: No results returned!")
except Exception as e:
print(f" Error: {e}")
print()
print("=" * 60)
print("Diagnostic Complete")
print("=" * 60)
if __name__ == "__main__":
check_database()

View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""
Fix the URL suffix UPDATE query to work regardless of userid value.
This script updates the query to not use WHERE userid = 'admin' if that's causing issues.
"""
# The issue is that the WHERE clause might not be matching
# Let's update the query to update ALL rows or use a different approach
# Option 1: Update all rows (if there's only one row)
# UPDATE admin_settings SET motm_url_suffix = :url_suffix
# Option 2: Update by id (if id is always 1)
# UPDATE admin_settings SET motm_url_suffix = :url_suffix WHERE id = 1
# Option 3: Use INSERT ... ON CONFLICT (PostgreSQL specific)
# INSERT INTO admin_settings (userid, motm_url_suffix)
# VALUES ('admin', :url_suffix)
# ON CONFLICT (userid) DO UPDATE SET motm_url_suffix = :url_suffix
print("This script documents the possible fixes for the URL suffix UPDATE issue.")
print()
print("The problem: UPDATE query returns True but doesn't actually update any rows")
print("Likely cause: WHERE userid = 'admin' is not matching any rows")
print()
print("Possible solutions:")
print("1. Update all rows: UPDATE admin_settings SET motm_url_suffix = :url_suffix")
print("2. Update by id: UPDATE admin_settings SET motm_url_suffix = :url_suffix WHERE id = 1")
print("3. Use UPSERT: INSERT ... ON CONFLICT (PostgreSQL)")
print()
print("Run the diagnostic script to see what's in the database first.")

View File

@ -361,3 +361,6 @@ For issues and questions:

View File

@ -241,3 +241,6 @@ For issues and questions, please refer to the application documentation or creat

View File

@ -258,3 +258,6 @@ main "$@"

View File

@ -67,3 +67,6 @@ Create the name of the service account to use

View File

@ -34,3 +34,6 @@ spec:

View File

@ -20,3 +20,6 @@ spec:

View File

@ -23,3 +23,6 @@ spec:

View File

@ -14,3 +14,6 @@ metadata:

View File

@ -265,11 +265,11 @@ def motm_vote(randomUrlSuffix):
nextTeam = nextInfo[0]['nextteam']
nextFixture = nextInfo[0]['nextfixture']
# Get HKFC logo from clubs table using signed URLs (with authentication)
hkfcLogo = s3_asset_service.get_asset_url('images/hkfc_logo.png') # Default fallback
sql_hkfc_logo = text("SELECT logo_url FROM clubs WHERE hockey_club = 'Hong Kong Football Club'")
hkfc_logo_result = sql_read(sql_hkfc_logo)
if hkfc_logo_result and hkfc_logo_result[0]['logo_url']:
hkfcLogo = s3_asset_service.get_logo_url(hkfc_logo_result[0]['logo_url'], 'Hong Kong Football Club')
hkfcLogo = s3_asset_service.get_asset_url('images/hkfclogo.png') # Default fallback
sql_hkfclogo = text("SELECT logo_url FROM clubs WHERE hockey_club = 'Hong Kong Football Club'")
hkfclogo_result = sql_read(sql_hkfclogo)
if hkfclogo_result and hkfclogo_result[0]['logo_url']:
hkfcLogo = s3_asset_service.get_logo_url(hkfclogo_result[0]['logo_url'], 'Hong Kong Football Club')
# Get opponent club logo from clubs table using signed URLs (with authentication)
oppoLogo = s3_asset_service.get_asset_url('images/default_logo.png') # Default fallback
@ -293,14 +293,14 @@ def motm_vote(randomUrlSuffix):
currMotM = None
currDotD = None
if nextInfo and nextInfo[0]['currmotm']:
sql3 = text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :curr_motm")
motm_result = sql_read(sql3, {'curr_motm': nextInfo[0]['currmotm']})
sql3 = text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :currmotm")
motm_result = sql_read(sql3, {'currmotm': nextInfo[0]['currmotm']})
if motm_result:
currMotM = motm_result[0]['playernickname']
if nextInfo and nextInfo[0]['currdotd']:
sql4 = text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :curr_dotd")
dotd_result = sql_read(sql4, {'curr_dotd': nextInfo[0]['currdotd']})
sql4 = text("SELECT playernickname FROM _hkfc_players WHERE playernumber = :currdotd")
dotd_result = sql_read(sql4, {'currdotd': nextInfo[0]['currdotd']})
if dotd_result:
currDotD = dotd_result[0]['playernickname']
@ -368,11 +368,11 @@ def match_comments():
_matchDate = '2025-01-15'
# Get HKFC logo from clubs table using signed URLs (with authentication)
hkfcLogo = s3_asset_service.get_asset_url('images/hkfc_logo.png') # Default fallback
sql_hkfc_logo = text("SELECT logo_url FROM clubs WHERE hockey_club = 'Hong Kong Football Club'")
hkfc_logo_result = sql_read(sql_hkfc_logo)
if hkfc_logo_result and hkfc_logo_result[0]['logo_url']:
hkfcLogo = s3_asset_service.get_logo_url(hkfc_logo_result[0]['logo_url'], 'Hong Kong Football Club')
hkfcLogo = s3_asset_service.get_asset_url('images/hkfclogo.png') # Default fallback
sql_hkfclogo = text("SELECT logo_url FROM clubs WHERE hockey_club = 'Hong Kong Football Club'")
hkfclogo_result = sql_read(sql_hkfclogo)
if hkfclogo_result and hkfclogo_result[0]['logo_url']:
hkfcLogo = s3_asset_service.get_logo_url(hkfclogo_result[0]['logo_url'], 'Hong Kong Football Club')
# Get opponent club logo from clubs table using signed URLs (with authentication)
oppoLogo = s3_asset_service.get_asset_url('images/default_logo.png') # Default fallback
@ -397,7 +397,7 @@ def vote_thanks():
"""Process MOTM/DotD votes and comments"""
try:
# Check voting deadline
sql_deadline = text("SELECT votingdeadline FROM motmadminsettings WHERE userid = 'admin'")
sql_deadline = text("SELECT votingdeadline FROM motmadminsettings")
deadline_result = sql_read_static(sql_deadline)
if deadline_result and deadline_result[0].get('votingdeadline'):
from datetime import datetime
@ -553,15 +553,19 @@ def motm_admin():
else:
prevFixture = str(prevFixture)
if request.method == 'POST':
print(f"DEBUG: POST request received")
print(f"DEBUG: form.saveButton.data = {form.saveButton.data}")
print(f"DEBUG: form.activateButton.data = {form.activateButton.data}")
if form.saveButton.data:
print('Saved')
print('DEBUG: Save button clicked')
else:
print('Activated')
print('DEBUG: Activate button clicked')
_nextTeam = request.form.get('nextOppoTeam', '')
_nextMatchDate = request.form.get('nextMatchDate', '')
_votingDeadline = request.form.get('votingDeadline', '')
_currMotM = request.form.get('currMotM', '0')
_currDotD = request.form.get('currDotD', '0')
print(f"DEBUG: Form data - team: {_nextTeam}, date: {_nextMatchDate}")
# Validate required fields
if not _nextTeam or not _nextMatchDate:
@ -584,8 +588,8 @@ def motm_admin():
# Get the form values for previous MOTM and DotD
# If user selected '0' (No Previous), use None
# Otherwise use the selected player number
curr_motm_value = None if _currMotM == '0' else _currMotM
curr_dotd_value = None if _currDotD == '0' else _currDotD
currmotm_value = None if _currMotM == '0' else _currMotM
currdotd_value = None if _currDotD == '0' else _currDotD
# Parse voting deadline if provided
voting_deadline_value = None
@ -596,13 +600,13 @@ def motm_admin():
except ValueError:
flash('Warning: Invalid voting deadline format. Deadline not set.', 'warning')
sql = text("UPDATE motmadminsettings SET nextdate = :next_date, nextclub = :next_club, nextteam = :next_team, currmotm = :curr_motm, currdotd = :curr_dotd, votingdeadline = :voting_deadline")
sql = text("UPDATE motmadminsettings SET nextdate = :nextdate, nextclub = :nextclub, nextteam = :nextteam, currmotm = :currmotm, currdotd = :currdotd, votingdeadline = :voting_deadline")
sql_write_static(sql, {
'next_date': _nextMatchDate,
'next_club': _nextClub,
'next_team': _nextTeam,
'curr_motm': curr_motm_value,
'curr_dotd': curr_dotd_value,
'nextdate': _nextMatchDate,
'nextclub': _nextClub,
'nextteam': _nextTeam,
'currmotm': currmotm_value,
'currdotd': currdotd_value,
'voting_deadline': voting_deadline_value
})
@ -614,14 +618,40 @@ def motm_admin():
sql_write_static(sql2, {'logo_url': logo_url})
else:
# Fallback to old method
sql2 = text("UPDATE motmadminsettings SET oppologo = (SELECT logo FROM menshockeyclubs WHERE hockeyclub = :next_club) WHERE nextclub = :next_club")
sql_write_static(sql2, {'next_club': _nextClub})
sql2 = text("UPDATE motmadminsettings SET oppologo = (SELECT logo FROM menshockeyclubs WHERE hockeyclub = :nextclub) WHERE nextclub = :nextclub")
sql_write_static(sql2, {'nextclub': _nextClub})
if form.saveButton.data:
flash('Settings saved!')
urlSuffix = randomUrlSuffix(8)
print(urlSuffix)
sql3 = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix WHERE userid = 'admin'")
sql_write_static(sql3, {'url_suffix': urlSuffix})
print(f"DEBUG: Generated URL suffix: {urlSuffix}")
# Check if row exists before update
check_row = sql_read_static(text("SELECT userid, motmurlsuffix FROM motmadminsettings"))
print(f"DEBUG: Rows in motmadminsettings: {check_row}")
# Try to update with WHERE userid = 'admin'
sql3 = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix")
print(f"DEBUG: About to execute UPDATE query with WHERE userid='admin'")
result = sql_write_static(sql3, {'url_suffix': urlSuffix})
print(f"DEBUG: UPDATE query result: {result}")
# Verify the update
verify = sql_read_static(text("SELECT motmurlsuffix FROM motmadminsettings"))
print(f"DEBUG: Verification with WHERE userid='admin': {verify}")
# If the update didn't work, try updating all rows (if there's only one row)
if not verify or not verify[0]['motmurlsuffix']:
print(f"DEBUG: First UPDATE didn't work, trying UPDATE without WHERE clause")
sql3_all = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix")
result_all = sql_write_static(sql3_all, {'url_suffix': urlSuffix})
print(f"DEBUG: UPDATE all rows result: {result_all}")
# Verify again
verify_all = sql_read_static(text("SELECT userid, motmurlsuffix FROM motmadminsettings"))
print(f"DEBUG: Verification after UPDATE all: {verify_all}")
if verify_all and verify_all[0]['motmurlsuffix']:
print(f"DEBUG: SUCCESS! URL suffix updated to: {verify_all[0]['motmurlsuffix']}")
flash('MotM URL https://motm.ervine.cloud/motm/'+urlSuffix)
elif form.activateButton.data:
# Generate a fixture number based on the date
@ -630,23 +660,42 @@ def motm_admin():
sql4 = text(f"ALTER TABLE _hkfc_c_motm ADD COLUMN motm_{_nextFixture} smallint DEFAULT 0, ADD COLUMN dotd_{_nextFixture} smallint DEFAULT 0, ADD COLUMN assists_{_nextFixture} smallint DEFAULT 0, ADD COLUMN goals_{_nextFixture} smallint DEFAULT 0")
sql_write(sql4)
except Exception as e:
# Columns already exist, which is fine
print(f"Columns already exist for fixture {_nextFixture}: {e}")
pass
sql5 = text("SELECT motmurlsuffix FROM motmadminsettings WHERE userid = 'admin'")
# Columns already exist, which is fine - just log at debug level
error_msg = str(e)
if 'already exists' in error_msg or 'duplicate' in error_msg.lower():
# This is expected if columns already exist, don't log as error
print(f"Columns for fixture {_nextFixture} already exist (this is normal)")
else:
# Unexpected error, log it
print(f"Error adding columns for fixture {_nextFixture}: {e}")
# Get or generate the URL suffix
print("DEBUG: Getting URL suffix from database")
sql5 = text("SELECT motmurlsuffix FROM motmadminsettings")
tempSuffix = sql_read_static(sql5)
if not tempSuffix:
flash('Error: Admin settings not found', 'error')
return redirect(url_for('motm_admin'))
currSuffix = tempSuffix[0]['motmurlsuffix']
print(currSuffix)
print(f"DEBUG: Query result: {tempSuffix}")
if not tempSuffix or not tempSuffix[0]['motmurlsuffix']:
# Generate a new URL suffix if one doesn't exist
print("DEBUG: URL suffix is None or empty, generating new one")
urlSuffix = randomUrlSuffix(8)
print(f"DEBUG: Generated new suffix: {urlSuffix}")
sql6 = text("UPDATE motmadminsettings SET motmurlsuffix = :url_suffix")
result = sql_write_static(sql6, {'url_suffix': urlSuffix})
print(f"DEBUG: UPDATE result: {result}")
currSuffix = urlSuffix
flash('New voting URL generated')
else:
currSuffix = tempSuffix[0]['motmurlsuffix']
print(f"DEBUG: Using existing suffix: {currSuffix}")
print(f"DEBUG: Final suffix: {currSuffix}")
flash('Man of the Match vote is now activated')
flash('MotM URL https://motm.ervine.cloud/motm/'+currSuffix)
else:
flash('Something went wrong - check with Smithers')
# Load current settings to populate the form
sql_current = text("SELECT nextdate, nextteam, currmotm, currdotd, votingdeadline FROM motmadminsettings WHERE userid = 'admin'")
sql_current = text("SELECT nextdate, nextteam, currmotm, currdotd, votingdeadline FROM motmadminsettings")
current_settings = sql_read_static(sql_current)
if current_settings:
from datetime import datetime
@ -1039,7 +1088,7 @@ def data_import():
if form.import_clubs.data:
# Import clubs based on Hong Kong Hockey Association data
clubs_data = [
{'hockey_club': 'HKFC', 'logo_url': '/static/images/hkfc_logo.png'},
{'hockey_club': 'HKFC', 'logo_url': '/static/images/hkfclogo.png'},
{'hockey_club': 'KCC', 'logo_url': '/static/images/kcc_logo.png'},
{'hockey_club': 'USRC', 'logo_url': '/static/images/usrc_logo.png'},
{'hockey_club': 'Valley', 'logo_url': '/static/images/valley_logo.png'},
@ -1792,7 +1841,7 @@ def matchSquadReset():
"""Reset squad for new match - archives current squad to history before clearing"""
try:
# Get current match date and fixture number from admin settings
sql_settings = text("SELECT nextdate, nextfixture FROM motmadminsettings WHERE userid = 'admin'")
sql_settings = text("SELECT nextdate, nextfixture FROM motmadminsettings")
settings = sql_read_static(sql_settings)
if not settings:
@ -1821,7 +1870,7 @@ def matchSquadReset():
sql_write(clear_sql)
# Update previous fixture number in settings
update_sql = text("UPDATE motmadminsettings SET prevfixture = :fixture_number WHERE userid = 'admin'")
update_sql = text("UPDATE motmadminsettings SET prevfixture = :fixture_number")
sql_write_static(update_sql, {'fixture_number': fixture_number})
flash(f'Squad reset successfully! {squad_count} players archived for match {fixture_number} ({match_date})', 'success')
@ -1872,7 +1921,7 @@ def goalsAssistsSubmit():
@app.route('/admin/api/next-fixture')
@basic_auth.required
def get_next_fixture():
def get_nextfixture():
"""API endpoint to fetch the next HKFC C fixture from Hockey Hong Kong website"""
try:
fixture = get_next_hkfc_c_fixture()
@ -2069,7 +2118,7 @@ def admin_fixture_logo_lookup(fixture):
def vote_results():
"""API endpoint for voting results"""
# Get the current match date from admin settings
sql_date = text("SELECT nextdate FROM motmadminsettings WHERE userid = 'admin'")
sql_date = text("SELECT nextdate FROM motmadminsettings")
date_result = sql_read_static(sql_date)
if not date_result:
@ -2133,7 +2182,7 @@ def poty_results():
def voting_chart():
"""Admin page for viewing voting charts"""
# Get the current match date from admin settings
sql_date = text("SELECT nextdate FROM motmadminsettings WHERE userid = 'admin'")
sql_date = text("SELECT nextdate FROM motmadminsettings")
date_result = sql_read_static(sql_date)
if date_result:

View File

@ -8,7 +8,7 @@ def mySettings(setting):
try:
# Convert setting to lowercase for PostgreSQL compatibility
setting_lower = setting.lower()
sql = text("SELECT " + setting_lower + " FROM motmadminsettings WHERE userid='admin'")
sql = text("SELECT " + setting_lower + " FROM motmadminsettings")
rows = sql_read_static(sql)
if rows:
return rows[0][setting_lower]

View File

@ -0,0 +1,13 @@
#!/bin/bash
# Script to revert all table and column name changes back to motmadminsettings
echo "This script will help you revert the table name changes."
echo "The issue is that production uses 'motmadminsettings' with camelCase columns,"
echo "but I changed everything to 'admin_settings' with snake_case columns."
echo ""
echo "You have two options:"
echo "1. Revert the code to use motmadminsettings (old table)"
echo "2. Migrate production database to use admin_settings (new table)"
echo ""
echo "Which would you prefer?"

View File

@ -0,0 +1,27 @@
#!/bin/bash
# Script to run the voting deadline migration on production
echo "=========================================="
echo "Running Voting Deadline Migration"
echo "=========================================="
echo ""
# Check if we're in a Kubernetes environment
if [ -n "$KUBERNETES_SERVICE_HOST" ]; then
echo "Running in Kubernetes environment"
python add_voting_deadline.py
else
echo "Running locally"
echo ""
echo "To run on production Kubernetes cluster:"
echo "1. kubectl get pods -n <your-namespace>"
echo "2. kubectl exec -it <pod-name> -n <your-namespace> -- python add_voting_deadline.py"
echo ""
echo "Or if using Helm:"
echo "helm upgrade motm-app ./helm-chart/motm-app --namespace <your-namespace>"
echo ""
python add_voting_deadline.py
fi
echo ""
echo "Migration complete!"

View File

@ -0,0 +1,102 @@
#!/bin/bash
# Quick script to run the voting deadline migration on production Kubernetes
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}MOTM App - Production Migration${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
# Default namespace
NAMESPACE="${1:-motm-app}"
echo -e "${YELLOW}Using namespace: ${NAMESPACE}${NC}"
echo ""
# Step 1: Find the pod
echo -e "${YELLOW}Step 1: Finding production pod...${NC}"
POD_NAME=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/name=motm-app -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [ -z "$POD_NAME" ]; then
echo -e "${RED}✗ Error: Could not find motm-app pod in namespace '$NAMESPACE'${NC}"
echo ""
echo "Available namespaces:"
kubectl get namespaces
echo ""
echo "Usage: $0 [namespace]"
echo "Example: $0 motm-app"
exit 1
fi
echo -e "${GREEN}✓ Found pod: ${POD_NAME}${NC}"
echo ""
# Step 2: Check if migration script exists in pod
echo -e "${YELLOW}Step 2: Checking migration script...${NC}"
if kubectl exec -n "$NAMESPACE" "$POD_NAME" -- test -f /app/add_voting_deadline.py 2>/dev/null; then
echo -e "${GREEN}✓ Migration script found${NC}"
else
echo -e "${RED}✗ Error: Migration script not found in pod${NC}"
echo "The add_voting_deadline.py script needs to be in the Docker image."
echo "Please rebuild and redeploy the application."
exit 1
fi
echo ""
# Step 3: Run the migration
echo -e "${YELLOW}Step 3: Running migration...${NC}"
if kubectl exec -n "$NAMESPACE" "$POD_NAME" -- python /app/add_voting_deadline.py; then
echo -e "${GREEN}✓ Migration completed successfully!${NC}"
else
echo -e "${RED}✗ Migration failed!${NC}"
exit 1
fi
echo ""
# Step 4: Verify the migration
echo -e "${YELLOW}Step 4: Verifying migration...${NC}"
VERIFY_CMD="from db_config import db_config; from sqlalchemy import text, inspect; engine = db_config.engine; inspector = inspect(engine); columns = inspector.get_columns('admin_settings'); voting_deadline_exists = any(col['name'] == 'votingdeadline' for col in columns); print('✓ votingdeadline column exists' if voting_deadline_exists else '✗ votingdeadline column missing')"
if kubectl exec -n "$NAMESPACE" "$POD_NAME" -- python -c "$VERIFY_CMD" 2>&1 | grep -q "✓"; then
echo -e "${GREEN}✓ Verification successful!${NC}"
else
echo -e "${RED}✗ Verification failed!${NC}"
echo "The column may not have been created properly."
exit 1
fi
echo ""
# Step 5: Restart the pod to clear cached connections
echo -e "${YELLOW}Step 5: Restarting pod to clear cached connections...${NC}"
kubectl rollout restart deployment/motm-app -n "$NAMESPACE" 2>/dev/null || \
kubectl delete pod "$POD_NAME" -n "$NAMESPACE"
echo -e "${GREEN}✓ Pod restart initiated${NC}"
echo ""
# Wait for pod to be ready
echo -e "${YELLOW}Waiting for pod to be ready...${NC}"
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=motm-app -n "$NAMESPACE" --timeout=120s
echo ""
# Summary
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}Migration Complete!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo "Next steps:"
echo "1. Visit https://motm.ervine.cloud/admin/motm"
echo "2. The page should load without SQL errors"
echo "3. You should see the 'Voting Deadline' field"
echo "4. Set a deadline for your next match"
echo ""
echo "To check logs:"
echo " kubectl logs -n $NAMESPACE -l app.kubernetes.io/name=motm-app --tail=50"
echo ""

View File

@ -10,35 +10,37 @@ class matchSquadTable:
def __html__(self):
"""Generate HTML table from items"""
if not self.items:
return Markup('<p>No players in squad</p>')
return Markup('<div class="alert alert-info text-center"><i class="fas fa-info-circle me-2"></i>No players in squad</div>')
# Start table
classes_str = ' '.join(self.classes) if self.classes else ''
border_attr = 'border="1"' if self.border else ''
html = f'<table class="table {classes_str}" {border_attr}>\n'
# Start table with Bootstrap 5 classes
classes_str = ' '.join(self.classes) if self.classes else 'table table-striped table-hover'
html = f'<div class="table-responsive"><table class="{classes_str} mb-0">\n'
# Table header
html += ' <thead>\n <tr>\n'
html += ' <th>Player Number</th>\n'
html += ' <th>Nickname</th>\n'
html += ' <th>Surname</th>\n'
html += ' <th>Forenames</th>\n'
html += ' <th>Delete</th>\n'
# Table header with modern styling
html += ' <thead class="table-dark">\n <tr>\n'
html += ' <th><i class="fas fa-hashtag me-1"></i>Player Number</th>\n'
html += ' <th><i class="fas fa-user me-1"></i>Nickname</th>\n'
html += ' <th><i class="fas fa-id-card me-1"></i>Surname</th>\n'
html += ' <th><i class="fas fa-id-card me-1"></i>Forenames</th>\n'
html += ' <th class="text-center"><i class="fas fa-cog me-1"></i>Actions</th>\n'
html += ' </tr>\n </thead>\n'
# Table body
# Table body with enhanced styling
html += ' <tbody>\n'
for item in self.items:
html += ' <tr>\n'
html += f' <td>{item.get("playernumber", "")}</td>\n'
html += f' <td>{item.get("playernickname", "")}</td>\n'
html += f' <td><span class="badge bg-primary fs-6">#{item.get("playernumber", "")}</span></td>\n'
html += f' <td><strong>{item.get("playernickname", "")}</strong></td>\n'
html += f' <td>{item.get("playersurname", "")}</td>\n'
html += f' <td>{item.get("playerforenames", "")}</td>\n'
html += f' <td><form method="post" action="/admin/squad/remove?playerNumber={item.get("playernumber", "")}"><button type="submit" class="btn btn-danger">Delete</button></form></td>\n'
html += f' <td class="text-center">'
html += f'<form method="post" action="/admin/squad/remove?playerNumber={item.get("playernumber", "")}" class="d-inline">'
html += f'<button type="submit" class="btn btn-danger btn-sm" data-confirm="Are you sure you want to remove player #{item.get("playernumber", "")} from the squad?">'
html += f'<i class="fas fa-trash me-1"></i>Remove</button></form></td>\n'
html += ' </tr>\n'
html += ' </tbody>\n'
# End table
html += '</table>\n'
html += '</table></div>\n'
return Markup(html)

View File

@ -1,73 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Match Squad Selection - HKFC Men's C Team</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>Match Squad Selection</h1>
<p class="lead">Select players for the match squad from the available players</p>
<div class="mb-3">
<a href="/admin/players" class="btn btn-outline-primary">Manage Players</a>
<a href="/admin" class="btn btn-secondary">Back to Admin</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% if players %}
<form action="/admin/squad/submit" method="post">
<div class="card">
<div class="card-header">
<h5>Available Players</h5>
<small class="text-muted">Select players to add to the match squad</small>
</div>
<div class="card-body">
<div class="row">
{% for player in players %}
<div class="col-md-4 mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="playerNumber" value="{{ player.playernumber }}" id="player{{ player.playernumber }}">
<label class="form-check-label" for="player{{ player.playernumber }}">
<strong>#{{ player.playernumber }}</strong> {{ player.playerforenames }} {{ player.playersurname }}
<br>
<small class="text-muted">{{ player.playernickname }} - {{ player.playerteam }}</small>
</label>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">Add Selected Players to Squad</button>
<a href="/admin/squad/list" class="btn btn-outline-secondary">View Current Squad</a>
</div>
</div>
</form>
{% else %}
<div class="alert alert-warning">
<h5>No players available</h5>
<p>There are no players in the database. <a href="/admin/players/add">Add some players</a> before selecting a squad.</p>
</div>
{% endif %}
</div>
</div>
{% extends "base.html" %}
{% block title %}Match Squad Selection - HKFC Men's C Team{% endblock %}
{% block content %}
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body text-center">
<h1 class="card-title">
<i class="fas fa-users text-primary me-2"></i>
Match Squad Selection
</h1>
<p class="lead text-muted">Select players for the match squad from the available players</p>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-bolt me-2"></i>Quick Actions
</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
<a href="/admin/players" class="btn btn-outline-primary">
<i class="fas fa-user-plus me-2"></i>Manage Players
</a>
<a href="/admin/squad/list" class="btn btn-outline-info">
<i class="fas fa-list me-2"></i>View Current Squad
</a>
<a href="/admin/squad/history" class="btn btn-outline-secondary">
<i class="fas fa-history me-2"></i>Squad History
</a>
<a href="/admin" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>Back to Admin
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Squad Selection Form -->
{% if players %}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">
<i class="fas fa-check-circle me-2"></i>Available Players
</h5>
<small class="opacity-75">Select players to add to the match squad</small>
</div>
<div class="card-body">
<form action="/admin/squad/submit" method="post">
<div class="row">
{% for player in players %}
<div class="col-md-4 col-lg-3 mb-3">
<div class="card h-100">
<div class="card-body p-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="playerNumber" value="{{ player.playernumber }}" id="player{{ player.playernumber }}">
<label class="form-check-label w-100" for="player{{ player.playernumber }}">
<div class="d-flex align-items-center">
<div class="player-number me-3">
<span class="badge bg-primary fs-6">#{{ player.playernumber }}</span>
</div>
<div class="player-info flex-grow-1">
<div class="fw-bold">{{ player.playerforenames }} {{ player.playersurname }}</div>
<small class="text-muted">{{ player.playernickname }}</small>
<br>
<small class="text-info">
<i class="fas fa-users me-1"></i>{{ player.playerteam }}
</small>
</div>
</div>
</label>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="mt-4 text-center">
<button type="submit" class="btn btn-success btn-lg">
<i class="fas fa-plus-circle me-2"></i>Add Selected Players to Squad
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% else %}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body text-center">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle fa-3x mb-3 text-warning"></i>
<h4>No Players Available</h4>
<p class="mb-3">There are no players in the database. You need to add some players before selecting a squad.</p>
<a href="/admin/players/add" class="btn btn-primary">
<i class="fas fa-user-plus me-2"></i>Add Players
</a>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_scripts %}
<script>
$(document).ready(function() {
// Select all functionality
$('#selectAll').on('change', function() {
$('input[name="playerNumber"]').prop('checked', this.checked);
});
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
// Update select all checkbox when individual checkboxes change
$('input[name="playerNumber"]').on('change', function() {
var totalCheckboxes = $('input[name="playerNumber"]').length;
var checkedCheckboxes = $('input[name="playerNumber"]:checked').length;
$('#selectAll').prop('checked', totalCheckboxes === checkedCheckboxes);
});
});
</script>
{% endblock %}

View File

@ -1,19 +1,49 @@
<html>
<head>
<title>Squad Reset</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Match squad has been reset for the next match</h3>
<a class="btn btn-primary" href="/admin/squad" role="button">Add Players to New Squad</a>
<a class="btn btn-danger" href="/" role="button">Home</a>
{% extends "base.html" %}
{% block title %}Squad Reset - HKFC Men's C Team{% endblock %}
{% block content %}
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body text-center">
<h1 class="card-title">
<i class="fas fa-refresh text-warning me-2"></i>
Squad Reset Complete
</h1>
<p class="lead text-muted">The match squad has been reset for the next match</p>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="card-title mb-0">
<i class="fas fa-bolt me-2"></i>Next Steps
</h5>
</div>
<div class="card-body text-center">
<div class="d-flex flex-wrap justify-content-center gap-3">
<a href="/admin/squad" class="btn btn-success btn-lg">
<i class="fas fa-plus-circle me-2"></i>Add Players to New Squad
</a>
<a href="/admin/squad/history" class="btn btn-info btn-lg">
<i class="fas fa-history me-2"></i>View Squad History
</a>
<a href="/admin" class="btn btn-primary btn-lg">
<i class="fas fa-tachometer-alt me-2"></i>Admin Dashboard
</a>
<a href="/" class="btn btn-secondary btn-lg">
<i class="fas fa-home me-2"></i>Home
</a>
</div>
</div>
</div>
</body>
</html>
</div>
</div>
{% endblock %}

View File

@ -1,40 +1,72 @@
<html>
<head>
<title>HKFC Men's C Team - Match Squad Selected</title>
<link rel="stylesheet" media="screen" href ="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-theme.min.css">
<meta name="viewport" content = "width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Match Squad</h3>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="mb-3">
<a class="btn btn-primary" href="/admin/squad" role="button">Add More Players</a>
<a class="btn btn-info" href="/admin/squad/list" role="button">View Squad List</a>
<a class="btn btn-warning" href="/admin/squad/reset" role="button">Reset Squad</a>
<a class="btn btn-secondary" href="/admin" role="button">Back to Admin</a>
<a class="btn btn-danger" href="/" role="button">Cancel</a>
</div>
{{ table }}
{% extends "base.html" %}
{% block title %}Match Squad - HKFC Men's C Team{% endblock %}
{% block content %}
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body text-center">
<h1 class="card-title">
<i class="fas fa-users text-success me-2"></i>
Current Match Squad
</h1>
<p class="lead text-muted">Manage the current match squad players</p>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-bolt me-2"></i>Squad Management
</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
<a href="/admin/squad" class="btn btn-success">
<i class="fas fa-plus-circle me-2"></i>Add More Players
</a>
<a href="/admin/squad/list" class="btn btn-info">
<i class="fas fa-list me-2"></i>Refresh Squad List
</a>
<a href="/admin/squad/history" class="btn btn-outline-secondary">
<i class="fas fa-history me-2"></i>Squad History
</a>
<a href="/admin/squad/reset" class="btn btn-warning" data-confirm="Are you sure you want to reset the squad? This will archive the current squad and clear it for the next match.">
<i class="fas fa-refresh me-2"></i>Reset Squad
</a>
<a href="/admin" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>Back to Admin
</a>
<a href="/" class="btn btn-outline-danger">
<i class="fas fa-home me-2"></i>Home
</a>
</div>
</div>
</div>
</body>
</html>
</div>
</div>
<!-- Squad Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">
<i class="fas fa-table me-2"></i>Squad Players
</h5>
<small class="opacity-75">Current players selected for the match</small>
</div>
<div class="card-body p-0">
{{ table }}
</div>
</div>
</div>
</div>
{% endblock %}