From 4a447e73a29c245473262797f4fe49b4c57bb44f Mon Sep 17 00:00:00 2001 From: 5t4l1n Date: Sun, 27 Jul 2025 00:34:18 +0530 Subject: [PATCH] panel & coding --- backend/main.py | 451 +++++++++-- backend/routes/exam.py | 191 +++++ frontend/app/coding/host/[examCode]/page.tsx | 763 ++++++++----------- frontend/app/coding/page.tsx | 37 +- 4 files changed, 944 insertions(+), 498 deletions(-) diff --git a/backend/main.py b/backend/main.py index 181488e..bdec388 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,6 +6,7 @@ import asyncio from mongo_service import MongoService from web3_service import Web3Service import logging +from datetime import datetime # Load environment variables first load_dotenv() @@ -75,11 +76,23 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) +# ✅ DEFINE check_docker_availability BEFORE using it +def check_docker_availability(): + """Check if Docker is available for compiler service""" + try: + import docker + client = docker.from_env() + client.ping() + return True + except Exception: + return False + # Initialize services with error handling try: mongo_service = MongoService(app.config['MONGODB_URI']) app.config['MONGO_SERVICE'] = mongo_service MONGO_SERVICE_AVAILABLE = True + logger.info("✅ MongoDB service initialized") except Exception as e: logging.error(f"Failed to initialize MongoDB service: {e}") mongo_service = None @@ -92,6 +105,7 @@ try: ) app.config['WEB3_SERVICE'] = web3_service WEB3_SERVICE_AVAILABLE = True + logger.info("✅ Web3 service initialized") except Exception as e: logging.error(f"Failed to initialize Web3 service: {e}") web3_service = None @@ -100,44 +114,229 @@ except Exception as e: # Make services available to routes if WALLET_SERVICE_AVAILABLE: app.config['WALLET_SERVICE'] = wallet_service + logger.info("✅ Wallet service available") if COMPILER_SERVICE_AVAILABLE: app.config['REAL_COMPILER_SERVICE'] = real_compiler_service + logger.info("✅ Compiler service available") -# ✅ DEFINE check_docker_availability BEFORE using it -def check_docker_availability(): - """Check if Docker is available for compiler service""" - try: - import docker - client = docker.from_env() - client.ping() - return True - except Exception: - return False - -# Register all blueprints with error handling +# ✅ ENHANCED: Register all blueprints with better error handling blueprints = [ - (auth.bp, '/api/auth'), - (test_flow.bp, '/api/test'), - (certificate.bp, '/api/certificate'), - (dashboard.bp, '/api/dashboard'), - (courses.bp, '/api/courses'), - (quizzes.bp, '/api/quizzes'), - (admin.bp, '/api/admin'), - (exam.bp, '/api/exam'), # Coding exam routes - (compiler.bp, '/api/compiler'), # Compiler routes + (auth.bp, '/api/auth', 'Authentication'), + (test_flow.bp, '/api/test', 'Test Flow'), + (certificate.bp, '/api/certificate', 'Certificates'), + (dashboard.bp, '/api/dashboard', 'Dashboard'), + (courses.bp, '/api/courses', 'Courses'), + (quizzes.bp, '/api/quizzes', 'Quizzes'), + (admin.bp, '/api/admin', 'Admin Panel'), + (exam.bp, '/api/exam', 'Coding Exams'), # ✅ Key for coding exam functionality + (compiler.bp, '/api/compiler', 'Code Compiler'), # ✅ Key for compiler functionality ] -for blueprint, url_prefix in blueprints: +registered_blueprints = [] +failed_blueprints = [] + +for blueprint, url_prefix, description in blueprints: try: app.register_blueprint(blueprint, url_prefix=url_prefix) - logging.info(f"✅ Registered blueprint: {url_prefix}") - print(f"✅ Registered blueprint: {url_prefix}") + registered_blueprints.append((description, url_prefix)) + logging.info(f"✅ Registered {description}: {url_prefix}") + print(f"✅ Registered {description}: {url_prefix}") except Exception as e: - logging.error(f"❌ Failed to register blueprint {url_prefix}: {e}") - print(f"❌ Failed to register blueprint {url_prefix}: {e}") + failed_blueprints.append((description, url_prefix, str(e))) + logging.error(f"❌ Failed to register {description} ({url_prefix}): {e}") + print(f"❌ Failed to register {description} ({url_prefix}): {e}") -# Debug routes +# ✅ CRITICAL FIX: Add missing exam info endpoint directly to main.py +@app.route('/api/exam/info/', methods=['GET', 'OPTIONS']) +def get_exam_info_direct(exam_code): + """Direct exam info endpoint for host panel - CRITICAL FIX""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") + return response + + try: + print(f"📊 DIRECT host panel request for exam: {exam_code}") + + # Get MongoDB connection + from pymongo import MongoClient + client = MongoClient(app.config['MONGODB_URI']) + db = client.openlearnx + + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + if not exam: + print(f"❌ Exam not found: {exam_code}") + return jsonify({"success": False, "error": "Exam not found"}), 404 + + # Convert datetime to string if needed + created_at = exam.get("created_at") + if hasattr(created_at, 'isoformat'): + created_at = created_at.isoformat() + elif created_at: + created_at = str(created_at) + + exam_info = { + "title": exam.get("title", "Untitled Exam"), + "status": exam.get("status", "waiting"), + "duration_minutes": exam.get("duration_minutes", 30), + "participants_count": len(exam.get("participants", [])), + "max_participants": exam.get("max_participants", 50), + "problem_title": exam.get("problem", {}).get("title", exam.get("title", "Coding Challenge")), + "languages": exam.get("problem", {}).get("languages", ["python"]), + "created_at": created_at, + "host_name": exam.get("host_name", "Unknown Host") + } + + print(f"✅ Found exam: {exam_info['title']} (Status: {exam_info['status']})") + return jsonify({"success": True, "exam_info": exam_info}) + + except Exception as e: + print(f"❌ Error getting exam info: {str(e)}") + import traceback + traceback.print_exc() + return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500 + +# ✅ ADD: Additional missing host panel endpoints +@app.route('/api/exam/participants/', methods=['GET', 'OPTIONS']) +def get_participants_direct(exam_code): + """Get participants for host panel""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") + return response + + try: + print(f"👥 DIRECT participants request for exam: {exam_code}") + + from pymongo import MongoClient + client = MongoClient(app.config['MONGODB_URI']) + db = client.openlearnx + + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + participants = exam.get("participants", []) + + # Format participant data + formatted_participants = [] + for participant in participants: + # Convert datetime to string if needed + joined_at = participant.get("joined_at") + if hasattr(joined_at, 'isoformat'): + joined_at = joined_at.isoformat() + elif joined_at: + joined_at = str(joined_at) + + submitted_at = participant.get("submitted_at") + if hasattr(submitted_at, 'isoformat'): + submitted_at = submitted_at.isoformat() + elif submitted_at: + submitted_at = str(submitted_at) + + participant_data = { + "name": participant.get("name", ""), + "score": participant.get("score", 0), + "completed": participant.get("completed", False), + "joined_at": joined_at, + "submitted_at": submitted_at + } + formatted_participants.append(participant_data) + + print(f"✅ Found {len(formatted_participants)} participants for exam {exam_code}") + return jsonify({"success": True, "participants": formatted_participants}) + + except Exception as e: + print(f"❌ Error getting participants: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/exam/remove-participant', methods=['POST', 'OPTIONS']) +def remove_participant_direct(): + """Remove participant from exam (host panel)""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") + return response + + try: + data = request.get_json() + exam_code = data.get('exam_code', '').upper() + participant_name = data.get('participant_name', '') + + print(f"🗑️ DIRECT remove participant request: {participant_name} from {exam_code}") + + if not exam_code or not participant_name: + return jsonify({"success": False, "error": "Missing exam_code or participant_name"}), 400 + + from pymongo import MongoClient + client = MongoClient(app.config['MONGODB_URI']) + db = client.openlearnx + + result = db.exams.update_one( + {"exam_code": exam_code}, + {"$pull": {"participants": {"name": participant_name}}} + ) + + if result.modified_count > 0: + print(f"✅ Removed participant {participant_name} from exam {exam_code}") + return jsonify({"success": True, "message": f"Participant {participant_name} removed successfully"}) + else: + return jsonify({"success": False, "error": "Participant not found or already removed"}), 404 + + except Exception as e: + print(f"❌ Error removing participant: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/exam/stop-exam', methods=['POST', 'OPTIONS']) +def stop_exam_direct(): + """Stop exam (host panel)""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") + return response + + try: + data = request.get_json() + exam_code = data.get('exam_code', '').upper() + + print(f"🛑 DIRECT stop exam request: {exam_code}") + + if not exam_code: + return jsonify({"success": False, "error": "Missing exam_code"}), 400 + + from pymongo import MongoClient + client = MongoClient(app.config['MONGODB_URI']) + db = client.openlearnx + + result = db.exams.update_one( + {"exam_code": exam_code}, + {"$set": { + "status": "completed", + "ended_at": datetime.now().isoformat(), + "ended_by": "host" + }} + ) + + if result.modified_count > 0: + print(f"✅ Exam {exam_code} stopped by host") + return jsonify({"success": True, "message": "Exam stopped successfully"}) + else: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + except Exception as e: + print(f"❌ Error stopping exam: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 + +# ✅ ENHANCED: Debug routes with better structure @app.route('/debug/routes') def debug_routes(): """Debug route to see all registered routes""" @@ -150,7 +349,15 @@ def debug_routes(): }) return jsonify({ "total_routes": len(routes), - "routes": sorted(routes, key=lambda x: x['rule']) + "routes": sorted(routes, key=lambda x: x['rule']), + "registered_blueprints": [desc for desc, url in registered_blueprints], + "failed_blueprints": failed_blueprints, + "direct_exam_routes_added": [ + "/api/exam/info/", + "/api/exam/participants/", + "/api/exam/remove-participant", + "/api/exam/stop-exam" + ] }) @app.route('/debug/exam-routes') @@ -164,9 +371,22 @@ def debug_exam_routes(): 'methods': list(rule.methods), 'rule': str(rule) }) + return jsonify({ "exam_routes": exam_routes, - "exam_blueprint_registered": hasattr(exam, 'bp') + "exam_blueprint_registered": any('Coding Exams' in desc for desc, _ in registered_blueprints), + "expected_exam_routes": [ + "/api/exam/create-exam", + "/api/exam/join-exam", + "/api/exam/start-exam", + "/api/exam/info/", + "/api/exam/participants/", + "/api/exam/remove-participant", + "/api/exam/stop-exam", + "/api/exam/leaderboard/", + "/api/exam/test" + ], + "direct_routes_added": True }) @app.route('/debug/services') @@ -181,10 +401,17 @@ def debug_services(): "docker": check_docker_availability() }, "app_config_keys": list(app.config.keys()), - "blueprint_count": len(app.blueprints) + "blueprint_count": len(app.blueprints), + "registered_blueprints": registered_blueprints, + "failed_blueprints": failed_blueprints, + "exam_system_ready": all([ + MONGO_SERVICE_AVAILABLE, + any('Coding Exams' in desc for desc, _ in registered_blueprints) + ]), + "direct_exam_endpoints_added": True }) -# Direct exam test route +# ✅ ENHANCED: Direct exam test route with better functionality @app.route('/api/exam/test-direct', methods=['GET', 'POST', 'OPTIONS']) def test_exam_direct(): """Direct test route for exam functionality""" @@ -199,22 +426,28 @@ def test_exam_direct(): "success": True, "message": "Direct exam route is working", "method": request.method, - "timestamp": os.popen('date').read().strip(), - "data": request.json if request.method == "POST" else None + "timestamp": datetime.now().isoformat(), + "data": request.json if request.method == "POST" else None, + "mongo_available": MONGO_SERVICE_AVAILABLE, + "exam_blueprint_registered": any('Coding Exams' in desc for desc, _ in registered_blueprints), + "direct_endpoints_added": True }) -# Health check endpoints +# ✅ ENHANCED: Health check endpoints @app.route('/') def health_check(): return jsonify({ "status": "OpenLearnX API is running", - "version": "2.0.0", + "version": "2.0.1", + "timestamp": datetime.now().isoformat(), "features": { "blockchain": WEB3_SERVICE_AVAILABLE, "coding_exams": COMPILER_SERVICE_AVAILABLE, "wallet_auth": WALLET_SERVICE_AVAILABLE, "database": MONGO_SERVICE_AVAILABLE, - "real_compiler": COMPILER_SERVICE_AVAILABLE + "real_compiler": COMPILER_SERVICE_AVAILABLE, + "docker": check_docker_availability(), + "direct_exam_endpoints": True }, "endpoints": { "auth": "/api/auth", @@ -231,7 +464,10 @@ def health_check(): "/debug/exam-routes", "/debug/services", "/api/exam/test-direct" - ] + ], + "registered_blueprints": len(registered_blueprints), + "failed_blueprints": len(failed_blueprints), + "host_panel_fix": "✅ Direct endpoints added" }) @app.route('/api/health') @@ -239,7 +475,7 @@ def api_health(): """Comprehensive API health check""" health_status = { "status": "healthy", - "timestamp": os.popen('date').read().strip(), + "timestamp": datetime.now().isoformat(), "services": { "mongodb": MONGO_SERVICE_AVAILABLE, "web3": WEB3_SERVICE_AVAILABLE, @@ -253,10 +489,24 @@ def api_health(): "secret_key_set": bool(app.config.get('SECRET_KEY')), "admin_token_set": bool(app.config.get('ADMIN_TOKEN')) }, - "blueprints_registered": list(app.blueprints.keys()) + "blueprints_registered": list(app.blueprints.keys()), + "exam_system": { + "ready": all([ + MONGO_SERVICE_AVAILABLE, + any('Coding Exams' in desc for desc, _ in registered_blueprints) + ]), + "features": [ + "Real-time exam creation", + "Student joining with exam codes", + "Host management panel", + "Live leaderboards", + "Multi-language compiler support" + ], + "host_panel_fix": "✅ Direct endpoints added to main.py" + } } - # Check MongoDB connection + # ✅ ENHANCED: Check MongoDB connection if MONGO_SERVICE_AVAILABLE: try: from pymongo import MongoClient @@ -267,7 +517,7 @@ def api_health(): health_status["services"]["mongodb_connection"] = f"error: {str(e)}" health_status["status"] = "degraded" - # Check Web3 connection + # ✅ ENHANCED: Check Web3 connection if WEB3_SERVICE_AVAILABLE: try: if web3_service and web3_service.w3.is_connected(): @@ -287,6 +537,7 @@ def admin_health(): """Admin-specific health check""" return jsonify({ "status": "Admin API is running", + "timestamp": datetime.now().isoformat(), "admin_token_configured": bool(app.config.get('ADMIN_TOKEN')), "admin_endpoints": [ "/api/admin/dashboard", @@ -302,7 +553,11 @@ def admin_health(): "/api/exam/start-exam", "/api/exam/submit-solution", "/api/exam/leaderboard/", - "/api/exam/host-dashboard/" + "/api/exam/host-dashboard/", + "/api/exam/info/", + "/api/exam/participants/", + "/api/exam/remove-participant", + "/api/exam/stop-exam" ], "compiler_endpoints": [ "/api/compiler/languages", @@ -311,15 +566,26 @@ def admin_health(): "/api/compiler/status/", "/api/compiler/test", "/api/compiler/stats" - ] + ], + "exam_system_status": { + "ready": all([ + MONGO_SERVICE_AVAILABLE, + any('Coding Exams' in desc for desc, _ in registered_blueprints) + ]), + "mongo_connected": MONGO_SERVICE_AVAILABLE, + "exam_routes_registered": any('Coding Exams' in desc for desc, _ in registered_blueprints), + "host_panel_endpoints": "✅ Added directly to main.py" + } }) -# Error handlers +# ✅ ENHANCED: Error handlers with better error information @app.errorhandler(404) def not_found(error): return jsonify({ "error": "Endpoint not found", "message": "The requested API endpoint does not exist", + "requested_path": request.path, + "method": request.method, "available_endpoints": [ "/api/auth", "/api/courses", "/api/admin", "/api/exam", "/api/dashboard", "/api/certificate", @@ -329,6 +595,13 @@ def not_found(error): "/debug/routes", "/debug/exam-routes", "/debug/services" + ], + "exam_specific_endpoints": [ + "/api/exam/create-exam", + "/api/exam/join-exam", + "/api/exam/info/", + "/api/exam/participants/", + "/api/exam/test-direct" ] }), 404 @@ -337,21 +610,24 @@ def internal_error(error): app.logger.error(f"Internal server error: {str(error)}") return jsonify({ "error": "Internal server error", - "message": "An unexpected error occurred on the server" + "message": "An unexpected error occurred on the server", + "timestamp": datetime.now().isoformat() }), 500 @app.errorhandler(403) def forbidden(error): return jsonify({ "error": "Forbidden", - "message": "Access denied - check your authentication" + "message": "Access denied - check your authentication", + "timestamp": datetime.now().isoformat() }), 403 @app.errorhandler(401) def unauthorized(error): return jsonify({ "error": "Unauthorized", - "message": "Authentication required" + "message": "Authentication required", + "timestamp": datetime.now().isoformat() }), 401 @app.errorhandler(Exception) @@ -359,10 +635,11 @@ def handle_error(error): app.logger.error(f"Unhandled error: {str(error)}") return jsonify({ "error": "An unexpected error occurred", - "type": type(error).__name__ + "type": type(error).__name__, + "timestamp": datetime.now().isoformat() }), 500 -# Request logging and CORS handling +# ✅ ENHANCED: Request logging and CORS handling @app.before_request def log_request_info(): """Enhanced request logging""" @@ -373,6 +650,10 @@ def log_request_info(): if '/api/exam' in request.path: logger.info(f"Exam request: {request.method} {request.path}") print(f"📝 Exam request: {request.method} {request.path}") + + # Log request data for debugging + if request.method in ['POST', 'PUT'] and request.json: + print(f"📦 Request data: {request.json}") if '/api/compiler' in request.path: logger.info(f"Compiler request: {request.method} {request.path}") @@ -396,13 +677,34 @@ def after_request(response): response.headers.add('X-XSS-Protection', '1; mode=block') return response -# Startup function +# ✅ ENHANCED: Startup function with comprehensive initialization def initialize_application(): """Initialize application with comprehensive error handling""" try: logger.info("🚀 Initializing OpenLearnX Backend...") print("🚀 Initializing OpenLearnX Backend...") + # ✅ Enhanced blueprint registration status + print(f"📋 Blueprint Registration Status:") + for description, url_prefix in registered_blueprints: + print(f" ✅ {description}: {url_prefix}") + + if failed_blueprints: + print(f"❌ Failed Blueprints:") + for description, url_prefix, error in failed_blueprints: + print(f" ❌ {description}: {url_prefix} - {error}") + + # ✅ Show direct endpoints added + print(f"🔧 Direct Exam Endpoints Added to main.py:") + direct_endpoints = [ + "/api/exam/info/", + "/api/exam/participants/", + "/api/exam/remove-participant", + "/api/exam/stop-exam" + ] + for endpoint in direct_endpoints: + print(f" ✅ {endpoint}") + # Test MongoDB connection if MONGO_SERVICE_AVAILABLE: try: @@ -447,7 +749,7 @@ def initialize_application(): logger.warning(f"⚠️ Docker connection error: {e}") print(f"⚠️ Docker connection error: {e}") - # Log configuration + # ✅ Enhanced configuration summary logger.info("📋 Configuration Summary:") print("📋 Configuration Summary:") config_items = [ @@ -455,7 +757,9 @@ def initialize_application(): ("Blockchain", WEB3_SERVICE_AVAILABLE), ("Wallet Service", WALLET_SERVICE_AVAILABLE), ("Compiler Service", COMPILER_SERVICE_AVAILABLE), - ("Docker", check_docker_availability()) + ("Docker", check_docker_availability()), + ("Exam System", any('Coding Exams' in desc for desc, _ in registered_blueprints)), + ("Host Panel Fix", True) ] for name, available in config_items: @@ -463,12 +767,13 @@ def initialize_application(): logger.info(f" • {name}: {status}") print(f" • {name}: {status}") - # Log access URLs + # ✅ Enhanced access URLs logger.info("🌐 Access URLs:") print("🌐 Access URLs:") urls = [ ("API Health", "http://127.0.0.1:5000/api/health"), + ("Main Page", "http://127.0.0.1:5000/"), ("Admin Panel", "http://localhost:3000/admin/login"), ("Coding Exams", "http://localhost:3000/coding"), ("Real Compiler", "http://localhost:3000/compiler"), @@ -480,18 +785,43 @@ def initialize_application(): logger.info(f" • {name}: {url}") print(f" • {name}: {url}") - # Debug URLs + # ✅ Enhanced debug URLs print("🔧 Debug URLs:") debug_urls = [ ("All Routes", "http://127.0.0.1:5000/debug/routes"), ("Exam Routes", "http://127.0.0.1:5000/debug/exam-routes"), ("Services", "http://127.0.0.1:5000/debug/services"), - ("Direct Test", "http://127.0.0.1:5000/api/exam/test-direct") + ("Direct Test", "http://127.0.0.1:5000/api/exam/test-direct"), + ("Admin Health", "http://127.0.0.1:5000/api/admin/health"), + ("Exam Info Test", "http://127.0.0.1:5000/api/exam/info/TEST123") ] for name, url in debug_urls: print(f" • {name}: {url}") + # ✅ Enhanced exam system status + exam_ready = all([ + MONGO_SERVICE_AVAILABLE, + any('Coding Exams' in desc for desc, _ in registered_blueprints) + ]) + + print(f"📝 Exam System Status: {'✅ Ready' if exam_ready else '❌ Not Ready'}") + print(f"🔧 Host Panel Fix: ✅ Direct endpoints added to main.py") + + if exam_ready: + print("📝 Exam Features Available:") + features = [ + "Create coding exams with 6-character codes", + "Students join with exam codes", + "Host management panel with live updates", + "Real-time participant monitoring", + "Live leaderboards with scoring", + "Multi-language code execution", + "Host panel endpoints working directly" + ] + for feature in features: + print(f" • {feature}") + # Log compiler features if COMPILER_SERVICE_AVAILABLE: logger.info("💻 Compiler Features:") @@ -507,7 +837,7 @@ def initialize_application(): logger.info(f" • {feature}") print(f" • {feature}") - # Log admin token (partially masked) + # ✅ Enhanced admin token logging admin_token = app.config.get('ADMIN_TOKEN', 'admin-secret-key') if admin_token: logger.info(f"🔑 Admin token configured: {admin_token[:8]}...") @@ -536,13 +866,20 @@ if __name__ == '__main__': logger.info("📚 Features: Blockchain Certificates, Coding Exams, Wallet Auth, Real Multi-language Compiler") print("📚 Features: Blockchain Certificates, Coding Exams, Wallet Auth, Real Multi-language Compiler") + # ✅ Enhanced server info + print(f"🌐 Server will be available at:") + print(f" • Local: http://127.0.0.1:5000") + print(f" • Network: http://0.0.0.0:5000") + print(f"📱 Frontend should connect to: http://127.0.0.1:5000") + print(f"🔧 Host Panel Fix: ✅ Direct endpoints added for exam info") + # Start Flask application app.run( debug=True, host='0.0.0.0', port=5000, threaded=True, - use_reloader=False + use_reloader=False # Prevent double initialization ) except KeyboardInterrupt: diff --git a/backend/routes/exam.py b/backend/routes/exam.py index c081663..084760f 100644 --- a/backend/routes/exam.py +++ b/backend/routes/exam.py @@ -458,6 +458,146 @@ def get_host_dashboard(exam_code): except Exception as e: return jsonify({"error": str(e)}), 500 +# ✅ CORRECTED: Host panel management endpoints (using Blueprint decorators) +@bp.route('/info/', methods=['GET', 'OPTIONS']) +def get_exam_info(exam_code): + """Get detailed information about an exam for the host panel""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") + return response + + try: + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + exam_info = { + "title": exam["title"], + "status": exam["status"], + "duration_minutes": exam["duration_minutes"], + "participants_count": len(exam.get("participants", [])), + "max_participants": exam["max_participants"], + "problem_title": exam.get("problem", {}).get("title", exam["title"]), + "languages": exam.get("problem", {}).get("languages", ["python"]), + "created_at": exam["created_at"], + "host_name": exam["host_name"] + } + + print(f"📊 Host panel requested info for exam {exam_code}") + return jsonify({"success": True, "exam_info": exam_info}) + except Exception as e: + print(f"❌ Error getting exam info: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 + +@bp.route('/participants/', methods=['GET', 'OPTIONS']) +def get_participants(exam_code): + """Get list of participants for host panel monitoring""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") + return response + + try: + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + participants = exam.get("participants", []) + + # Format participant data for host panel + formatted_participants = [] + for participant in participants: + participant_data = { + "name": participant.get("name", ""), + "score": participant.get("score", 0), + "completed": participant.get("completed", False), + "joined_at": participant.get("joined_at", ""), + "submitted_at": participant.get("submitted_at", None) + } + formatted_participants.append(participant_data) + + print(f"👥 Retrieved {len(formatted_participants)} participants for exam {exam_code}") + return jsonify({"success": True, "participants": formatted_participants}) + except Exception as e: + print(f"❌ Error getting participants: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 + +@bp.route('/remove-participant', methods=['POST', 'OPTIONS']) +def remove_participant(): + """Remove a participant from an exam (host only)""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") + return response + + try: + data = request.get_json() + exam_code = data.get('exam_code', '').upper() + participant_name = data.get('participant_name', '') + + if not exam_code or not participant_name: + return jsonify({"success": False, "error": "Missing exam_code or participant_name"}), 400 + + # Remove participant from exam + result = db.exams.update_one( + {"exam_code": exam_code}, + {"$pull": {"participants": {"name": participant_name}}} + ) + + if result.modified_count > 0: + print(f"🗑️ Host removed participant {participant_name} from exam {exam_code}") + return jsonify({"success": True, "message": f"Participant {participant_name} removed successfully"}) + else: + return jsonify({"success": False, "error": "Participant not found or already removed"}), 404 + + except Exception as e: + print(f"❌ Error removing participant: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 + +@bp.route('/stop-exam', methods=['POST', 'OPTIONS']) +def stop_exam(): + """Stop an exam early (host only)""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") + return response + + try: + data = request.get_json() + exam_code = data.get('exam_code', '').upper() + + if not exam_code: + return jsonify({"success": False, "error": "Missing exam_code"}), 400 + + # Update exam status to completed + result = db.exams.update_one( + {"exam_code": exam_code}, + {"$set": { + "status": "completed", + "ended_at": datetime.now().isoformat(), + "ended_by": "host" + }} + ) + + if result.modified_count > 0: + print(f"🛑 Exam {exam_code} stopped early by host") + return jsonify({"success": True, "message": "Exam stopped successfully"}) + else: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + except Exception as e: + print(f"❌ Error stopping exam: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 + @bp.route("/debug-join-data", methods=["POST", "OPTIONS"]) def debug_join_data(): """Debug what data is actually being received""" @@ -493,6 +633,10 @@ def test_exam_route(): "/api/exam/leaderboard/", "/api/exam/get-problem/", "/api/exam/host-dashboard/", + "/api/exam/info/", + "/api/exam/participants/", + "/api/exam/remove-participant", + "/api/exam/stop-exam", "/api/exam/debug-join-data" ] }) @@ -509,7 +653,54 @@ def exam_root(): "/api/exam/leaderboard/", "/api/exam/get-problem/", "/api/exam/host-dashboard/", + "/api/exam/info/", + "/api/exam/participants/", + "/api/exam/remove-participant", + "/api/exam/stop-exam", "/api/exam/test", "/api/exam/debug-join-data" ] }) +@bp.route('/info/', methods=['GET', 'OPTIONS']) +def get_exam_info(exam_code): + """Get detailed information about an exam for the host panel""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") + response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") + return response + + try: + print(f"📊 Host panel requesting info for exam: {exam_code}") + + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + if not exam: + print(f"❌ Exam not found: {exam_code}") + return jsonify({"success": False, "error": "Exam not found"}), 404 + + # Convert datetime objects to strings for JSON serialization + created_at = exam.get("created_at") + if hasattr(created_at, 'isoformat'): + created_at = created_at.isoformat() + + exam_info = { + "title": exam["title"], + "status": exam["status"], + "duration_minutes": exam["duration_minutes"], + "participants_count": len(exam.get("participants", [])), + "max_participants": exam.get("max_participants", 50), + "problem_title": exam.get("problem", {}).get("title", exam["title"]), + "languages": exam.get("problem", {}).get("languages", ["python"]), + "created_at": created_at, + "host_name": exam["host_name"] + } + + print(f"✅ Found exam: {exam['title']} (Status: {exam['status']})") + return jsonify({"success": True, "exam_info": exam_info}) + + except Exception as e: + print(f"❌ Error getting exam info: {str(e)}") + import traceback + traceback.print_exc() + return jsonify({"success": False, "error": str(e)}), 500 diff --git a/frontend/app/coding/host/[examCode]/page.tsx b/frontend/app/coding/host/[examCode]/page.tsx index d3f2532..5503628 100644 --- a/frontend/app/coding/host/[examCode]/page.tsx +++ b/frontend/app/coding/host/[examCode]/page.tsx @@ -1,87 +1,114 @@ 'use client' import React, { useState, useEffect } from 'react' -import { useParams, useRouter } from 'next/navigation' +import { useRouter, useParams } from 'next/navigation' import { - Users, Trophy, Clock, Play, Square, UserX, AlertTriangle, - RefreshCw, Settings, BarChart, Eye, Trash2, Plus, Timer + Users, + Trophy, + Clock, + Play, + Pause, + Square, + UserMinus, + RefreshCw, + Settings, + Monitor, + AlertCircle } from 'lucide-react' interface Participant { name: string - joined_at: string score: number completed: boolean - language?: string - submission_time?: string - rank?: number - kicked?: boolean + submitted_at?: string + joined_at: string } -interface ExamData { - exam_info: { - exam_code: string - title: string - status: string - duration_minutes: number - max_participants: number - time_elapsed: number - time_remaining: number - start_time?: string - end_time?: string - } - participants: { - total: number - completed: number - working: number - all_participants: Participant[] - recent_joins: Participant[] - } - leaderboard: Participant[] - statistics: { - average_score: number - highest_score: number - lowest_score: number - completion_rate: number - } +interface ExamInfo { + title: string + status: 'waiting' | 'active' | 'completed' + duration_minutes: number + participants_count: number + max_participants: number + problem_title: string + languages: string[] + created_at: string + host_name: string } -export default function HostDashboard() { +export default function HostPanel() { const params = useParams() const router = useRouter() const examCode = params.examCode as string - const [examData, setExamData] = useState(null) + const [examInfo, setExamInfo] = useState(null) + const [participants, setParticipants] = useState([]) + const [leaderboard, setLeaderboard] = useState([]) + const [timeRemaining, setTimeRemaining] = useState(0) const [loading, setLoading] = useState(true) - const [activeTab, setActiveTab] = useState<'overview' | 'participants' | 'leaderboard' | 'settings'>('overview') - const [selectedParticipant, setSelectedParticipant] = useState(null) - const [showKickModal, setShowKickModal] = useState(false) - const [refreshInterval, setRefreshInterval] = useState(3000) // 3 seconds + const [error, setError] = useState('') useEffect(() => { - if (!examCode) return + if (examCode) { + fetchExamInfo() + fetchParticipants() + fetchLeaderboard() + + // Auto-refresh every 5 seconds + const interval = setInterval(() => { + fetchParticipants() + fetchLeaderboard() + }, 5000) - fetchDashboardData() - const interval = setInterval(fetchDashboardData, refreshInterval) - return () => clearInterval(interval) - }, [examCode, refreshInterval]) + return () => clearInterval(interval) + } + }, [examCode]) - const fetchDashboardData = async () => { + const fetchExamInfo = async () => { try { - const response = await fetch(`http://127.0.0.1:5000/api/exam/host-dashboard/${examCode}`) + const response = await fetch(`http://127.0.0.1:5000/api/exam/info/${examCode}`) const data = await response.json() if (data.success) { - setExamData(data) + setExamInfo(data.exam_info) + if (data.exam_info.status === 'active') { + startTimer(data.exam_info.duration_minutes * 60) + } } else { - console.error('Failed to fetch dashboard data:', data.error) + setError('Failed to load exam information') } } catch (error) { - console.error('Error fetching dashboard data:', error) + setError('Network error') } finally { setLoading(false) } } + const fetchParticipants = async () => { + try { + const response = await fetch(`http://127.0.0.1:5000/api/exam/participants/${examCode}`) + const data = await response.json() + + if (data.success) { + setParticipants(data.participants) + } + } catch (error) { + console.error('Failed to fetch participants') + } + } + + const fetchLeaderboard = async () => { + try { + const response = await fetch(`http://127.0.0.1:5000/api/exam/leaderboard/${examCode}`) + const data = await response.json() + + if (data.success) { + setLeaderboard(data.leaderboard) + } + } catch (error) { + console.error('Failed to fetch leaderboard') + } + } + const startExam = async () => { try { const response = await fetch('http://127.0.0.1:5000/api/exam/start-exam', { @@ -89,91 +116,85 @@ export default function HostDashboard() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ exam_code: examCode }) }) - + const data = await response.json() if (data.success) { - alert('Exam started successfully!') - fetchDashboardData() + setExamInfo(prev => prev ? { ...prev, status: 'active' } : null) + startTimer(examInfo?.duration_minutes ? examInfo.duration_minutes * 60 : 1800) + alert('✅ Exam started! Participants can now begin coding.') } else { - alert(`Failed to start exam: ${data.error}`) + alert(`❌ Failed to start exam: ${data.error}`) } } catch (error) { - alert('Failed to start exam') + alert('❌ Network error occurred') } } - const endExam = async () => { - if (!confirm('Are you sure you want to end the exam? This cannot be undone.')) return - + const stopExam = async () => { try { - const response = await fetch('http://127.0.0.1:5000/api/exam/end-exam', { + const response = await fetch('http://127.0.0.1:5000/api/exam/stop-exam', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ exam_code: examCode }) }) - + const data = await response.json() if (data.success) { - alert('Exam ended successfully!') - fetchDashboardData() + setExamInfo(prev => prev ? { ...prev, status: 'completed' } : null) + setTimeRemaining(0) + alert('🛑 Exam stopped successfully!') } else { - alert(`Failed to end exam: ${data.error}`) + alert(`❌ Failed to stop exam: ${data.error}`) } } catch (error) { - alert('Failed to end exam') - } - } - - const extendExam = async (minutes: number) => { - try { - const response = await fetch('http://127.0.0.1:5000/api/exam/extend-exam', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - exam_code: examCode, - additional_minutes: minutes - }) - }) - - const data = await response.json() - if (data.success) { - alert(`Exam extended by ${minutes} minutes!`) - fetchDashboardData() - } else { - alert(`Failed to extend exam: ${data.error}`) - } - } catch (error) { - alert('Failed to extend exam') + alert('❌ Network error occurred') } } const removeParticipant = async (participantName: string) => { - if (!confirm(`Are you sure you want to remove "${participantName}" from the exam?`)) return + if (!confirm(`Are you sure you want to remove "${participantName}" from the exam?`)) { + return + } try { const response = await fetch('http://127.0.0.1:5000/api/exam/remove-participant', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ + body: JSON.stringify({ exam_code: examCode, participant_name: participantName }) }) - + const data = await response.json() if (data.success) { - alert(`Participant "${participantName}" removed successfully!`) - fetchDashboardData() - setShowKickModal(false) - setSelectedParticipant(null) + alert(`✅ Removed "${participantName}" from the exam`) + fetchParticipants() + fetchLeaderboard() } else { - alert(`Failed to remove participant: ${data.error}`) + alert(`❌ Failed to remove participant: ${data.error}`) } } catch (error) { - alert('Failed to remove participant') + alert('❌ Network error occurred') } } + const startTimer = (seconds: number) => { + setTimeRemaining(seconds) + + const timer = setInterval(() => { + setTimeRemaining(prev => { + if (prev <= 1) { + clearInterval(timer) + alert('⏰ Time is up! Exam has ended.') + setExamInfo(prev => prev ? { ...prev, status: 'completed' } : null) + return 0 + } + return prev - 1 + }) + }, 1000) + } + const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60) const secs = seconds % 60 @@ -182,10 +203,10 @@ export default function HostDashboard() { const getStatusColor = (status: string) => { switch (status) { - case 'waiting': return 'bg-yellow-100 text-yellow-800' - case 'active': return 'bg-green-100 text-green-800' - case 'completed': return 'bg-gray-100 text-gray-800' - default: return 'bg-gray-100 text-gray-800' + case 'waiting': return 'bg-yellow-600' + case 'active': return 'bg-green-600' + case 'completed': return 'bg-red-600' + default: return 'bg-gray-600' } } @@ -193,25 +214,25 @@ export default function HostDashboard() { return (
-
-

Loading host dashboard...

+ +

Loading host panel...

) } - if (!examData) { + if (error || !examInfo) { return (
- -

Exam Not Found

-

The exam code "{examCode}" is invalid or expired.

+ +

Error

+

{error || 'Exam not found'}

@@ -221,377 +242,247 @@ export default function HostDashboard() { return (
{/* Header */} -
-
-
-
-

{examData.exam_info.title}

-
- - CODE: {examData.exam_info.exam_code} - - - {examData.exam_info.status.toUpperCase()} - - - {examData.participants.total}/{examData.exam_info.max_participants} participants - +
+
+
+

+ + Host Panel +

+

Managing exam: {examCode}

+
+ +
+
+ {examInfo.status.toUpperCase()} +
+ + {timeRemaining > 0 && ( +
+ + {formatTime(timeRemaining)} +
+ )} +
+
+
+ +
+ {/* Main Content */} +
+ {/* Exam Info Cards */} +
+
+
+ +
+

Participants

+

{examInfo.participants_count}/{examInfo.max_participants}

+
-
- {/* Timer */} - {examData.exam_info.status === 'active' && examData.exam_info.time_remaining > 0 && ( -
- - {formatTime(examData.exam_info.time_remaining)} +
+
+ +
+

Duration

+

{examInfo.duration_minutes}m

- )} +
+
- {/* Control Buttons */} - {examData.exam_info.status === 'waiting' && ( +
+
+ +
+

Completed

+

{leaderboard.filter(p => p.completed).length}

+
+
+
+ +
+
+ +
+

Problem

+

{examInfo.problem_title}

+
+
+
+
+ + {/* Control Panel */} +
+

Exam Controls

+ +
+ {examInfo.status === 'waiting' && ( )} - {examData.exam_info.status === 'active' && ( -
+ {examInfo.status === 'active' && ( + <> - -
+ )} + +
-
-
- -
- {/* Tab Navigation */} -
- {[ - { id: 'overview', label: 'Overview', icon: BarChart }, - { id: 'participants', label: 'Participants', icon: Users }, - { id: 'leaderboard', label: 'Leaderboard', icon: Trophy }, - { id: 'settings', label: 'Settings', icon: Settings } - ].map(tab => ( - - ))} -
- - {/* Tab Content */} - {activeTab === 'overview' && ( -
- {/* Statistics Cards */} -
-
-
-
-

Total Participants

-

{examData.participants.total}

-
- -
-
- -
-
-
-

Completed

-

{examData.participants.completed}

-
- -
-
- -
-
-
-

Still Working

-

{examData.participants.working}

-
- -
-
- -
-
-
-

Average Score

-

- {Math.round(examData.statistics.average_score)}% -

-
- -
-
-
- - {/* Recent Activity */} -
-

Recent Participants

-
- {examData.participants.recent_joins.slice(0, 5).map((participant, index) => ( -
-
-
- {participant.name.charAt(0).toUpperCase()} -
-
-
{participant.name}
-
- Joined {new Date(participant.joined_at).toLocaleTimeString()} -
-
-
-
-
- {participant.completed ? `${participant.score}% completed` : 'Working'} -
-
-
- ))} -
-
-
- )} - - {activeTab === 'participants' && ( -
-
-
-

All Participants ({examData.participants.total})

-
- Completion Rate: {Math.round(examData.statistics.completion_rate)}% -
-
-
+ {/* Participants List */} +
+

+ + Participants ({participants.length}) +

+
- - - - - - - - + + + + + + + - - {examData.participants.all_participants.map((participant) => ( - - + {participants.map((participant, index) => ( + + + - - - - - ))}
ParticipantStatusScoreLanguageJoinedActions
NameJoined AtStatusScoreActions
-
-
- {participant.name.charAt(0).toUpperCase()} -
-
{participant.name}
-
+
{participant.name} + {new Date(participant.joined_at).toLocaleTimeString()} - + - {participant.kicked ? 'Kicked' : participant.completed ? 'Completed' : 'Working'} + {participant.completed ? 'Completed' : 'In Progress'} - {participant.completed ? `${participant.score}%` : '-'} + + {participant.completed ? ( + {participant.score}% + ) : ( + - + )} - {participant.language || '-'} - - {new Date(participant.joined_at).toLocaleString()} - -
- -
+
+
-
-
- )} - - {activeTab === 'leaderboard' && ( -
-

Live Leaderboard

-
- {examData.leaderboard.map((participant, index) => { - const rankColors = { - 1: 'bg-gradient-to-r from-yellow-600 to-yellow-500 text-white', - 2: 'bg-gradient-to-r from-gray-400 to-gray-500 text-white', - 3: 'bg-gradient-to-r from-orange-600 to-orange-500 text-white' - } - - return ( -
-
-
-
#{participant.rank}
-
-
{participant.name}
-
- {participant.language && `${participant.language} • `} - Submitted: {new Date(participant.submission_time!).toLocaleTimeString()} -
-
-
-
-
{participant.score}%
-
-
-
- ) - })} -
-
- )} - - {activeTab === 'settings' && ( -
-
-

Exam Controls

-
- - - - - -
-
- -
-

Auto-Refresh Settings

-
- - -
-
-
- )} -
- - {/* Kick Participant Modal */} - {showKickModal && selectedParticipant && ( -
-
-

Remove Participant

-

- Are you sure you want to remove "{selectedParticipant}" from the exam? - This action cannot be undone. -

-
- - + + {participants.length === 0 && ( +
+ No participants have joined yet. +
+ )}
- )} + + {/* Leaderboard Sidebar */} +
+
+ +

Live Leaderboard

+
+ +
+ {leaderboard.map((participant, index) => ( +
+
+
+
+ #{index + 1} + {participant.name} +
+ {participant.submitted_at && ( +

+ Submitted: {new Date(participant.submitted_at).toLocaleTimeString()} +

+ )} +
+
+
{participant.score}%
+
+ {participant.completed ? 'Completed' : 'In Progress'} +
+
+
+
+ ))} +
+ + {leaderboard.length === 0 && ( +
+ No submissions yet. +
+ )} + + +
+
) } diff --git a/frontend/app/coding/page.tsx b/frontend/app/coding/page.tsx index a69cf4b..078cfb7 100644 --- a/frontend/app/coding/page.tsx +++ b/frontend/app/coding/page.tsx @@ -104,6 +104,7 @@ export default function CodingExamPlatform() { } } + // ✅ UPDATED CREATE EXAM WITH HOST PANEL REDIRECT const createExam = async () => { try { const response = await fetch('http://127.0.0.1:5000/api/exam/create-exam', { @@ -113,7 +114,8 @@ export default function CodingExamPlatform() { title: 'String Capitalizer Challenge', problem_id: 'string-capitalizer', duration_minutes: 30, - host_name: participantName + host_name: participantName, + max_participants: 50 }) }) @@ -128,14 +130,38 @@ export default function CodingExamPlatform() { setExamId(participantCode) setExamInfo({ title: 'String Capitalizer Challenge', status: 'waiting' }) - // ✅ FIXED: Show exam_code instead of exam_id - alert(`Exam created! Share this code with participants: ${participantCode}`) + // Store host exam data + localStorage.setItem('host_exam', JSON.stringify({ + exam_code: participantCode, + exam_id: databaseId, + host_name: participantName, + created_at: new Date().toISOString(), + exam_details: data.exam_details || {} + })) + + // ✅ ENHANCED SUCCESS MESSAGE WITH REDIRECT INFO + alert(`✅ Exam Created Successfully! + +📝 Exam Code: ${participantCode} +📋 Title: String Capitalizer Challenge +👤 Host: ${participantName} +⏱️ Duration: 30 minutes + +🔗 Share this code with participants: ${participantCode} + +Redirecting to Host Management Panel...`) + + // ✅ REDIRECT TO HOST PANEL + setTimeout(() => { + router.push(`/coding/host/${participantCode}`) + }, 2000) + } else { - alert(`Failed to create exam: ${data.error}`) + alert(`❌ Failed to create exam: ${data.error}`) } } catch (error) { console.error('Create exam error:', error) - alert('Failed to create exam - network error') + alert('❌ Failed to create exam - network error') } } @@ -316,6 +342,7 @@ Redirecting to exam interface...`)

Will create with host_name: "{participantName}"

✅ Will display exam_code (6 chars), not exam_id

+

🔄 After creation → redirect to /coding/host/[examCode]