From a92c2401dd4584cc2ce3b9e32be5b9112f4bb322 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 24 Oct 2025 13:41:52 -0400 Subject: [PATCH] Fix: Add MySQL 5.6 compatibility and detailed error logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added better error handling and MySQL 5.6 support for Windows Server: 1. Enhanced error logging in app.py: - Detailed database error messages with error codes - Full stack traces logged to console/PM2 - Error details returned in JSON for debugging 2. Created app-pymysql.py: - Alternative version using PyMySQL instead of mysql-connector-python - Better compatibility with older MySQL 5.6 servers - Handles bit field conversion from bytes to boolean - Pure Python implementation (no C extensions) 3. Added requirements-mysql56.txt: - PyMySQL 1.1.0 for MySQL 5.6 compatibility - Use this on Windows servers with old MySQL For production Windows servers with MySQL 5.6, use: pip install -r requirements-mysql56.txt python app-pymysql.py For debugging 500 errors, check console/PM2 logs for detailed error messages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app-pymysql.py | 144 +++++++++++++++++++++++++++++++++++++++ app.py | 23 +++++-- requirements-frozen.txt | 9 +++ requirements-mysql56.txt | 3 + shopfloor-dashboard.zip | Bin 0 -> 12892 bytes 5 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 app-pymysql.py create mode 100644 requirements-frozen.txt create mode 100644 requirements-mysql56.txt create mode 100644 shopfloor-dashboard.zip diff --git a/app-pymysql.py b/app-pymysql.py new file mode 100644 index 0000000..eff3d45 --- /dev/null +++ b/app-pymysql.py @@ -0,0 +1,144 @@ +from flask import Flask, jsonify, send_from_directory +import pymysql +from datetime import datetime, timedelta +import os + +app = Flask(__name__, static_folder='public') + +# Database configuration +DB_CONFIG = { + 'host': os.environ.get('DB_HOST', 'localhost'), + 'port': int(os.environ.get('DB_PORT', 3306)), + 'user': os.environ.get('DB_USER', '570005354'), + 'password': os.environ.get('DB_PASS', '570005354'), + 'database': os.environ.get('DB_NAME', 'shopdb'), + 'charset': 'utf8mb4', + 'cursorclass': pymysql.cursors.DictCursor +} + +# Get database connection +def get_db_connection(): + return pymysql.connect(**DB_CONFIG) + +# Serve static files (index.html, logo, etc) +@app.route('/') +def index(): + return send_from_directory('public', 'index.html') + +@app.route('/') +def static_files(path): + return send_from_directory('public', path) + +# API endpoint to get notifications +@app.route('/api/notifications') +def get_notifications(): + try: + now = datetime.now() + future = now + timedelta(hours=72) # 72 hours from now + + # Connect to database + conn = get_db_connection() + cursor = conn.cursor() + + # Query with isshopfloor filter and 72-hour window + query = """ + SELECT n.notificationid, n.notification, n.starttime, n.endtime, n.ticketnumber, n.link, + n.isactive, n.isshopfloor, nt.typename, nt.typecolor + FROM notifications n + LEFT JOIN notificationtypes nt ON n.notificationtypeid = nt.notificationtypeid + WHERE n.isactive = 1 + AND n.isshopfloor = 1 + AND ( + (n.starttime <= %s AND (n.endtime IS NULL OR n.endtime >= %s)) + OR (n.starttime BETWEEN %s AND %s) + ) + ORDER BY n.starttime ASC + """ + + cursor.execute(query, (future, now, now, future)) + rows = cursor.fetchall() + + cursor.close() + conn.close() + + # Convert bit fields to boolean and datetime to ISO strings + for row in rows: + # PyMySQL returns bytes for bit fields, convert to boolean + row['isactive'] = bool(row['isactive'][0]) if isinstance(row['isactive'], bytes) else bool(row['isactive']) + row['isshopfloor'] = bool(row['isshopfloor'][0]) if isinstance(row['isshopfloor'], bytes) else bool(row['isshopfloor']) + + if row['starttime']: + row['starttime'] = row['starttime'].isoformat() if hasattr(row['starttime'], 'isoformat') else str(row['starttime']) + if row['endtime']: + row['endtime'] = row['endtime'].isoformat() if hasattr(row['endtime'], 'isoformat') else str(row['endtime']) + + # Categorize notifications + current_events = [] + upcoming_events = [] + + for notification in rows: + start = datetime.fromisoformat(notification['starttime']) + end = datetime.fromisoformat(notification['endtime']) if notification['endtime'] else None + + if start <= now and (end is None or end >= now): + current_events.append(notification) + else: + upcoming_events.append(notification) + + # Sort current events by severity priority, then by starttime + severity_priority = { + 'danger': 1, + 'warning': 2, + 'success': 3, + 'secondary': 4 + } + + current_events.sort(key=lambda x: ( + severity_priority.get(x['typecolor'], 4), + datetime.fromisoformat(x['starttime']) + )) + + upcoming_events.sort(key=lambda x: datetime.fromisoformat(x['starttime'])) + + return jsonify({ + 'success': True, + 'timestamp': datetime.now().isoformat(), + 'current': current_events, + 'upcoming': upcoming_events + }) + + except pymysql.Error as err: + import traceback + error_details = { + 'success': False, + 'error': f'Database error: {str(err)}', + 'error_code': err.args[0] if err.args else None, + 'traceback': traceback.format_exc() + } + print(f"DATABASE ERROR: {error_details}") + return jsonify(error_details), 500 + except Exception as e: + import traceback + error_details = { + 'success': False, + 'error': f'Server error: {str(e)}', + 'type': type(e).__name__, + 'traceback': traceback.format_exc() + } + print(f"SERVER ERROR: {error_details}") + return jsonify(error_details), 500 + +# Health check endpoint +@app.route('/health') +def health(): + return jsonify({ + 'status': 'ok', + 'timestamp': datetime.now().isoformat() + }) + +if __name__ == '__main__': + port = int(os.environ.get('PORT', 3001)) + print(f'Shopfloor Dashboard running on port {port}') + print(f'Access at: http://localhost:{port}') + print(f'Using PyMySQL connector for MySQL 5.6 compatibility') + app.run(host='0.0.0.0', port=port, debug=False) diff --git a/app.py b/app.py index fe90a50..1c7c644 100644 --- a/app.py +++ b/app.py @@ -106,15 +106,26 @@ def get_notifications(): }) except mysql.connector.Error as err: - return jsonify({ + import traceback + error_details = { 'success': False, - 'error': f'Database error: {str(err)}' - }), 500 + 'error': f'Database error: {str(err)}', + 'error_code': err.errno if hasattr(err, 'errno') else None, + 'error_msg': err.msg if hasattr(err, 'msg') else str(err), + 'traceback': traceback.format_exc() + } + print(f"DATABASE ERROR: {error_details}") # Log to console/PM2 + return jsonify(error_details), 500 except Exception as e: - return jsonify({ + import traceback + error_details = { 'success': False, - 'error': f'Server error: {str(e)}' - }), 500 + 'error': f'Server error: {str(e)}', + 'type': type(e).__name__, + 'traceback': traceback.format_exc() + } + print(f"SERVER ERROR: {error_details}") # Log to console/PM2 + return jsonify(error_details), 500 # Health check endpoint @app.route('/health') diff --git a/requirements-frozen.txt b/requirements-frozen.txt new file mode 100644 index 0000000..760f3d9 --- /dev/null +++ b/requirements-frozen.txt @@ -0,0 +1,9 @@ +blinker==1.9.0 +click==8.3.0 +Flask==3.0.0 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +mysql-connector-python==8.2.0 +protobuf==4.21.12 +Werkzeug==3.1.3 diff --git a/requirements-mysql56.txt b/requirements-mysql56.txt new file mode 100644 index 0000000..5d49252 --- /dev/null +++ b/requirements-mysql56.txt @@ -0,0 +1,3 @@ +Flask==3.0.0 +PyMySQL==1.1.0 +# Alternative to mysql-connector-python for better MySQL 5.6 compatibility diff --git a/shopfloor-dashboard.zip b/shopfloor-dashboard.zip new file mode 100644 index 0000000000000000000000000000000000000000..11cacb8aa6b86cecec902a42522aecf9f109f7c6 GIT binary patch literal 12892 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 M0sx>h{QdO50IfAgH~;_u literal 0 HcmV?d00001