diff --git a/app/app.py b/app/app.py index 4dc1e1f..ab01d2b 100644 --- a/app/app.py +++ b/app/app.py @@ -95,6 +95,17 @@ def handle_exception(e): def rate_limit_error(e): return render_template("429.html"), 429 + @app.route('/static/manifest.json') + def serve_manifest(): + return send_from_directory(app.static_folder, 'manifest.json') + + @app.route('/static/js/service-worker.js') + def serve_service_worker(): + response = make_response(send_from_directory(app.static_folder, 'js/service-worker.js')) + response.headers['Service-Worker-Allowed'] = '/' + response.headers['Cache-Control'] = 'no-cache' + return response + return app diff --git a/app/scout/routes.py b/app/scout/routes.py index 3479e55..b213cae 100644 --- a/app/scout/routes.py +++ b/app/scout/routes.py @@ -35,9 +35,13 @@ def add(): return render_template("scouting/add.html") data = request.get_json() if request.is_json else request.form.to_dict() - if "auto_path" in data and isinstance(data["auto_path"], str): + if "auto_path" in data: try: - data["auto_path"] = json.loads(data["auto_path"]) + if isinstance(data["auto_path"], str): + if data["auto_path"].strip(): + data["auto_path"] = json.loads(data["auto_path"]) + else: + data["auto_path"] = [] except json.JSONDecodeError: flash("Invalid path coordinates format", "error") return redirect(url_for("scouting.home")) @@ -58,7 +62,10 @@ def add(): @login_required def home(): try: - team_data = scouting_manager.get_all_scouting_data() + team_data = scouting_manager.get_all_scouting_data( + current_user.teamNumber, + current_user.get_id() + ) return render_template("scouting/list.html", team_data=team_data) except Exception as e: current_app.logger.error(f"Error fetching scouting data: {str(e)}", exc_info=True) @@ -168,47 +175,24 @@ def compare_teams(): teams_data = {} for team_num in teams: try: - # Get team stats from database + # Add team access filter to pipeline pipeline = [ {"$match": {"team_number": int(team_num)}}, - {"$group": { - "_id": "$team_number", - "matches_played": {"$sum": 1}, - "auto_coral_level1": {"$avg": {"$ifNull": ["$auto_coral_level1", 0]}}, - "auto_coral_level2": {"$avg": {"$ifNull": ["$auto_coral_level2", 0]}}, - "auto_coral_level3": {"$avg": {"$ifNull": ["$auto_coral_level3", 0]}}, - "auto_coral_level4": {"$avg": {"$ifNull": ["$auto_coral_level4", 0]}}, - "auto_algae_net": {"$avg": {"$ifNull": ["$auto_algae_net", 0]}}, - "auto_algae_processor": {"$avg": {"$ifNull": ["$auto_algae_processor", 0]}}, - "teleop_coral_level1": {"$avg": {"$ifNull": ["$teleop_coral_level1", 0]}}, - "teleop_coral_level2": {"$avg": {"$ifNull": ["$teleop_coral_level2", 0]}}, - "teleop_coral_level3": {"$avg": {"$ifNull": ["$teleop_coral_level3", 0]}}, - "teleop_coral_level4": {"$avg": {"$ifNull": ["$teleop_coral_level4", 0]}}, - "teleop_algae_net": {"$avg": {"$ifNull": ["$teleop_algae_net", 0]}}, - "teleop_algae_processor": {"$avg": {"$ifNull": ["$teleop_algae_processor", 0]}}, - "auto_path": {"$last": "$auto_path"}, - "climb_success_rate": { - "$avg": {"$cond": [{"$eq": ["$climb_success", True]}, 100, 0]} - }, - "preferred_climb_type": {"$last": "$climb_type"}, - "total_coral": {"$sum": { - "$add": [ - "$auto_coral_level1", "$auto_coral_level2", - "$auto_coral_level3", "$auto_coral_level4", - "$teleop_coral_level1", "$teleop_coral_level2", - "$teleop_coral_level3", "$teleop_coral_level4" - ] - }}, - "total_algae": {"$sum": { - "$add": [ - "$auto_algae_net", "$auto_algae_processor", - "$teleop_algae_net", "$teleop_algae_processor" - ] - }}, - "defense_rating": {"$avg": "$defense_rating"}, - "successful_climbs": { - "$sum": {"$cond": ["$climb_success", 1, 0]} - }, + {"$lookup": { + "from": "users", + "localField": "scouter_id", + "foreignField": "_id", + "as": "scouter" + }}, + {"$unwind": "$scouter"}, + # Fix team access filter + {"$match": { + "$or": [ + {"scouter.teamNumber": current_user.teamNumber}, + {"scouter._id": ObjectId(current_user.get_id())} + ] if current_user.teamNumber else { + "scouter._id": ObjectId(current_user.get_id()) + } }} ] @@ -309,7 +293,6 @@ def compare_teams(): } except Exception as team_error: - # print(f"Error processing team {team_num}: {str(team_error)}") teams_data[team_num] = { "team_number": int(team_num), "error": str(team_error) @@ -318,7 +301,6 @@ def compare_teams(): return json.loads(json_util.dumps(teams_data)) except Exception as e: - # print(f"Error in compare_teams: {str(e)}") return jsonify({"error": "An error occurred while comparing teams"}), 500 @scouting_bp.route("/search") @@ -369,15 +351,20 @@ async def search_teams(): # Fetch scouting data from our database pipeline = [ {"$match": {"team_number": team_number}}, - { - "$lookup": { - "from": "users", - "localField": "scouter_id", - "foreignField": "_id", - "as": "scouter" - } - }, - {"$unwind": {"path": "$scouter", "preserveNullAndEmptyArrays": True}}, + {"$lookup": { + "from": "users", + "localField": "scouter_id", + "foreignField": "_id", + "as": "scouter" + }}, + {"$unwind": {"path": "$scouter"}}, + # Add team access filter + {"$match": { + "$or": [ + {"scouter.teamNumber": current_user.teamNumber} if current_user.teamNumber else {"scouter._id": ObjectId(current_user.get_id())}, + {"scouter._id": ObjectId(current_user.get_id())} + ] + }}, {"$sort": {"event_code": 1, "match_number": 1}}, { "$project": { @@ -597,36 +584,49 @@ def matches(): try: pipeline = [ { - "$group": { - "_id": { - "event": "$event_code", - "match": "$match_number" - }, - "teams": { - "$push": { - "number": "$team_number", - "alliance": "$alliance", - # Auto period - "auto_coral_level1": {"$ifNull": ["$auto_coral_level1", 0]}, - "auto_coral_level2": {"$ifNull": ["$auto_coral_level2", 0]}, - "auto_coral_level3": {"$ifNull": ["$auto_coral_level3", 0]}, - "auto_coral_level4": {"$ifNull": ["$auto_coral_level4", 0]}, - "auto_algae_net": {"$ifNull": ["$auto_algae_net", 0]}, - "auto_algae_processor": {"$ifNull": ["$auto_algae_processor", 0]}, - # Teleop period - "teleop_coral_level1": {"$ifNull": ["$teleop_coral_level1", 0]}, - "teleop_coral_level2": {"$ifNull": ["$teleop_coral_level2", 0]}, - "teleop_coral_level3": {"$ifNull": ["$teleop_coral_level3", 0]}, - "teleop_coral_level4": {"$ifNull": ["$teleop_coral_level4", 0]}, - "teleop_algae_net": {"$ifNull": ["$teleop_algae_net", 0]}, - "teleop_algae_processor": {"$ifNull": ["$teleop_algae_processor", 0]}, - "climb_type": "$climb_type", - "climb_success": "$climb_success" - } - }, + "$lookup": { + "from": "users", + "localField": "scouter_id", + "foreignField": "_id", + "as": "scouter" } }, - {"$sort": {"_id.event": 1, "_id.match": 1}} + {"$unwind": "$scouter"}, + # Add team access filter + {"$match": { + "$or": [ + {"scouter.teamNumber": current_user.teamNumber} if current_user.teamNumber else {"scouter._id": ObjectId(current_user.get_id())}, + {"scouter._id": ObjectId(current_user.get_id())} + ] + }}, + {"$group": { + "_id": { + "event": "$event_code", + "match": "$match_number" + }, + "teams": { + "$push": { + "number": "$team_number", + "alliance": "$alliance", + # Auto period + "auto_coral_level1": {"$ifNull": ["$auto_coral_level1", 0]}, + "auto_coral_level2": {"$ifNull": ["$auto_coral_level2", 0]}, + "auto_coral_level3": {"$ifNull": ["$auto_coral_level3", 0]}, + "auto_coral_level4": {"$ifNull": ["$auto_coral_level4", 0]}, + "auto_algae_net": {"$ifNull": ["$auto_algae_net", 0]}, + "auto_algae_processor": {"$ifNull": ["$auto_algae_processor", 0]}, + # Teleop period + "teleop_coral_level1": {"$ifNull": ["$teleop_coral_level1", 0]}, + "teleop_coral_level2": {"$ifNull": ["$teleop_coral_level2", 0]}, + "teleop_coral_level3": {"$ifNull": ["$teleop_coral_level3", 0]}, + "teleop_coral_level4": {"$ifNull": ["$teleop_coral_level4", 0]}, + "teleop_algae_net": {"$ifNull": ["$teleop_algae_net", 0]}, + "teleop_algae_processor": {"$ifNull": ["$teleop_algae_processor", 0]}, + "climb_type": "$climb_type", + "climb_success": "$climb_success" + } + }, + }} ] match_data = list(scouting_manager.db.team_data.aggregate(pipeline)) @@ -698,7 +698,7 @@ def matches(): }) return render_template("scouting/matches.html", matches=matches) - + except Exception as e: current_app.logger.error(f"Error fetching matches: {str(e)}", exc_info=True) flash("An internal error has occurred.", "error") @@ -710,22 +710,41 @@ def check_team(): team_number = request.args.get('team') event_code = request.args.get('event') match_number = request.args.get('match') - current_id = request.args.get('current_id') # ID of the entry being edited + current_id = request.args.get('current_id') try: - query = { - "team_number": int(team_number), - "event_code": event_code, - "match_number": int(match_number) - } + # Get current user's team number + current_user_team = current_user.teamNumber + + pipeline = [ + { + "$match": { + "team_number": int(team_number), + "event_code": event_code, + "match_number": int(match_number) + } + }, + { + "$lookup": { + "from": "users", + "localField": "scouter_id", + "foreignField": "_id", + "as": "scouter" + } + }, + {"$unwind": "$scouter"}, + ] - # If editing, exclude the current entry from the check if current_id: - query["_id"] = {"$ne": ObjectId(current_id)} + pipeline[0]["$match"]["_id"] = {"$ne": ObjectId(current_id)} - existing = scouting_manager.db.team_data.find_one(query) + existing = list(scouting_manager.db.team_data.aggregate(pipeline)) - return jsonify({"exists": existing is not None}) + # Check if any existing entry is from the same team + exists = any(entry.get("scouter", {}).get("teamNumber") == current_user_team + for entry in existing) + + return jsonify({"exists": exists}) except Exception as e: current_app.logger.error(f"Error checking team data: {str(e)}", exc_info=True) return jsonify({"error": "An internal error has occurred."}), 500 @@ -735,7 +754,11 @@ def check_team(): @limiter.limit("30 per minute") def pit_scouting(): try: - pit_data_list = list(scouting_manager.get_all_pit_scouting()) + # Update to use filtered pit scouting data + pit_data_list = list(scouting_manager.get_all_pit_scouting( + current_user.teamNumber, + current_user.get_id() + )) return render_template("scouting/pit-scouting.html", pit_data=pit_data_list) except Exception as e: current_app.logger.error(f"Error fetching pit scouting data: {str(e)}", exc_info=True) @@ -747,82 +770,87 @@ def pit_scouting(): @limiter.limit("10 per minute") def pit_scouting_add(): if request.method == "POST": - # Process form data - pit_data = { - "team_number": int(request.form.get("team_number")), - "scouter_id": current_user.id, - - # Drive base information - "drive_type": { - "swerve": "swerve" in request.form.getlist("drive_type"), - "tank": "tank" in request.form.getlist("drive_type"), - "other": request.form.get("drive_type_other", "") - }, - "swerve_modules": request.form.get("swerve_modules", ""), - - # Motor details - "motor_details": { - "falcons": "falcons" in request.form.getlist("motors"), - "neos": "neos" in request.form.getlist("motors"), - "krakens": "krakens" in request.form.getlist("motors"), - "vortex": "vortex" in request.form.getlist("motors"), - "other": request.form.get("motors_other", "") - }, - "motor_count": int(request.form.get("motor_count", 0)), - - # Dimensions - "dimensions": { - "length": float(request.form.get("length", 0)), - "width": float(request.form.get("width", 0)), - "height": float(request.form.get("height", 0)) - }, - - # Mechanisms - "mechanisms": { - "coral_scoring": { - "enabled": request.form.get("coral_scoring_enabled") == "true", - "notes": request.form.get("coral_scoring_notes", "") if request.form.get("coral_scoring_enabled") == "true" else "" + try: + # Process form data + pit_data = { + "team_number": int(request.form.get("team_number")), + "scouter_id": current_user.id, + + # Drive base information + "drive_type": { + "swerve": "swerve" in request.form.getlist("drive_type"), + "tank": "tank" in request.form.getlist("drive_type"), + "other": request.form.get("drive_type_other", "") + }, + "swerve_modules": request.form.get("swerve_modules", ""), + + # Motor details + "motor_details": { + "falcons": "falcons" in request.form.getlist("motors"), + "neos": "neos" in request.form.getlist("motors"), + "krakens": "krakens" in request.form.getlist("motors"), + "vortex": "vortex" in request.form.getlist("motors"), + "other": request.form.get("motors_other", "") }, - "algae_scoring": { - "enabled": request.form.get("algae_scoring_enabled") == "true", - "notes": request.form.get("algae_scoring_notes", "") if request.form.get("algae_scoring_enabled") == "true" else "" + "motor_count": int(request.form.get("motor_count", 0)), + + # Dimensions + "dimensions": { + "length": float(request.form.get("length", 0)), + "width": float(request.form.get("width", 0)), + "height": float(request.form.get("height", 0)) }, - "climber": { - "has_climber": "has_climber" in request.form, - "type_climber": request.form.get("climber_type", ""), - "notes": request.form.get("climber_notes", "") - } - }, - - # Programming and Autonomous - "programming_language": request.form.get("programming_language", ""), - "autonomous_capabilities": { - "has_auto": request.form.get("has_auto") == "true", - "num_routes": int(request.form.get("auto_routes", 0)) if request.form.get("has_auto") == "true" else 0, - "preferred_start": request.form.get("auto_preferred_start", "") if request.form.get("has_auto") == "true" else "", - "notes": request.form.get("auto_notes", "") if request.form.get("has_auto") == "true" else "" - }, - - # Driver Experience - "driver_experience": { - "years": int(request.form.get("driver_years", 0)), - "notes": request.form.get("driver_notes", "") - }, - - # General Notes - "notes": request.form.get("notes", ""), - - # Timestamps - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc) - } - - # Add to database - if scouting_manager.add_pit_scouting(pit_data): - flash("Pit scouting data added successfully!", "success") + + # Mechanisms + "mechanisms": { + "coral_scoring": { + "enabled": request.form.get("coral_scoring_enabled") == "true", + "notes": request.form.get("coral_scoring_notes", "") if request.form.get("coral_scoring_enabled") == "true" else "" + }, + "algae_scoring": { + "enabled": request.form.get("algae_scoring_enabled") == "true", + "notes": request.form.get("algae_scoring_notes", "") if request.form.get("algae_scoring_enabled") == "true" else "" + }, + "climber": { + "has_climber": "has_climber" in request.form, + "type_climber": request.form.get("climber_type", ""), + "notes": request.form.get("climber_notes", "") + } + }, + + # Programming and Autonomous + "programming_language": request.form.get("programming_language", ""), + "autonomous_capabilities": { + "has_auto": request.form.get("has_auto") == "true", + "num_routes": int(request.form.get("auto_routes", 0)) if request.form.get("has_auto") == "true" else 0, + "preferred_start": request.form.get("auto_preferred_start", "") if request.form.get("has_auto") == "true" else "", + "notes": request.form.get("auto_notes", "") if request.form.get("has_auto") == "true" else "" + }, + + # Driver Experience + "driver_experience": { + "years": int(request.form.get("driver_years", 0)), + "notes": request.form.get("driver_notes", "") + }, + + # General Notes + "notes": request.form.get("notes", ""), + + # Timestamps + "created_at": datetime.now(timezone.utc), + "updated_at": datetime.now(timezone.utc) + } + + # Add to database + if scouting_manager.add_pit_scouting(pit_data): + flash("Pit scouting data added successfully!", "success") + return redirect(url_for("scouting.pit_scouting")) + else: + flash("Error adding pit scouting data. Please try again.", "error") + except Exception as e: + flash("An error occurred while adding pit scouting data.", "error") + current_app.logger.error(f"Error adding pit scouting data: {str(e)}", exc_info=True) return redirect(url_for("scouting.pit_scouting")) - else: - flash("Error adding pit scouting data. Please try again.", "error") return render_template("scouting/pit-scouting-add.html") diff --git a/app/scout/scouting_utils.py b/app/scout/scouting_utils.py index 4d08ae0..edb4c79 100644 --- a/app/scout/scouting_utils.py +++ b/app/scout/scouting_utils.py @@ -67,14 +67,39 @@ def add_scouting_data(self, data, scouter_id): if team_number <= 0: return False, "Invalid team number" - if existing_entry := self.db.team_data.find_one( + # Lookup to check if this team is already scouted in this match by someone from the same team + pipeline = [ + { + "$match": { + "event_code": data["event_code"], + "match_number": int(data["match_number"]), + "team_number": team_number + } + }, { - "event_code": data["event_code"], - "match_number": int(data["match_number"]), - "team_number": team_number, + "$lookup": { + "from": "users", + "localField": "scouter_id", + "foreignField": "_id", + "as": "scouter" + } + }, + {"$unwind": "$scouter"}, + { + "$lookup": { + "from": "users", + "localField": "scouter.teamNumber", + "foreignField": "teamNumber", + "as": "team_scouters" + } } - ): - return False, f"Team {team_number} already exists in match {data['match_number']} for event {data['event_code']}" + ] + + if existing_entries := list(self.db.team_data.aggregate(pipeline)): + current_user = self.db.users.find_one({"_id": ObjectId(scouter_id)}) + for entry in existing_entries: + if entry.get("scouter", {}).get("teamNumber") == current_user.get("teamNumber"): + return False, f"Team {team_number} has already been scouted by your team in match {data['match_number']}" # Get existing match data to validate alliance sizes and calculate scores match_data = list(self.db.team_data.find({ @@ -96,42 +121,42 @@ def add_scouting_data(self, data, scouter_id): "event_code": data["event_code"], "match_number": int(data["match_number"]), "alliance": alliance, - + # Auto Coral scoring "auto_coral_level1": int(data.get("auto_coral_level1", 0)), "auto_coral_level2": int(data.get("auto_coral_level2", 0)), "auto_coral_level3": int(data.get("auto_coral_level3", 0)), "auto_coral_level4": int(data.get("auto_coral_level4", 0)), - + # Teleop Coral scoring "teleop_coral_level1": int(data.get("teleop_coral_level1", 0)), "teleop_coral_level2": int(data.get("teleop_coral_level2", 0)), "teleop_coral_level3": int(data.get("teleop_coral_level3", 0)), "teleop_coral_level4": int(data.get("teleop_coral_level4", 0)), - + # Auto Algae scoring "auto_algae_net": int(data.get("auto_algae_net", 0)), "auto_algae_processor": int(data.get("auto_algae_processor", 0)), - + # Teleop Algae scoring "teleop_algae_net": int(data.get("teleop_algae_net", 0)), "teleop_algae_processor": int(data.get("teleop_algae_processor", 0)), - + # Climb "climb_type": data.get("climb_type", ""), "climb_success": bool(data.get("climb_success", False)), - + # Defense "defense_rating": int(data.get("defense_rating", 1)), "defense_notes": data.get("defense_notes", ""), - + # Auto "auto_path": data.get("auto_path", ""), "auto_notes": data.get("auto_notes", ""), - + # Notes "notes": data.get("notes", ""), - + # Metadata "scouter_id": ObjectId(scouter_id), "created_at": datetime.now(timezone.utc), @@ -145,9 +170,10 @@ def add_scouting_data(self, data, scouter_id): return False, "An internal error has occurred." @with_mongodb_retry(retries=3, delay=2) - def get_all_scouting_data(self): - """Get all scouting data with user information""" + def get_all_scouting_data(self, user_team_number=None, user_id=None): + """Get all scouting data with user information, filtered by team access""" try: + # Base pipeline for user lookup pipeline = [ { "$lookup": { @@ -158,38 +184,60 @@ def get_all_scouting_data(self): } }, {"$unwind": "$scouter"}, - { - "$project": { - "_id": 1, - "team_number": 1, - "match_number": 1, - "event_code": 1, - "auto_coral_level1": 1, - "auto_coral_level2": 1, - "auto_coral_level3": 1, - "auto_coral_level4": 1, - "teleop_coral_level1": 1, - "teleop_coral_level2": 1, - "teleop_coral_level3": 1, - "teleop_coral_level4": 1, - "auto_algae_net": 1, - "auto_algae_processor": 1, - "teleop_algae_net": 1, - "teleop_algae_processor": 1, - "climb_type": 1, - "climb_success": 1, - "defense_rating": 1, - "defense_notes": 1, - "auto_path": 1, - "auto_notes": 1, - "notes": 1, - "alliance": 1, - "scouter_id": 1, - "scouter_name": "$scouter.username", - "scouter_team": "$scouter.teamNumber" + ] + + # Add match stage for filtering based on team number or user ID + if user_team_number: + # If user has a team number, show data from their team and their own data + pipeline.append({ + "$match": { + "$or": [ + {"scouter.teamNumber": user_team_number}, + {"scouter._id": ObjectId(user_id)} + ] + } + }) + else: + # If user has no team, only show their own data + pipeline.append({ + "$match": { + "scouter._id": ObjectId(user_id) } + }) + + # Project the needed fields + pipeline.append({ + "$project": { + "_id": 1, + "team_number": 1, + "match_number": 1, + "event_code": 1, + "auto_coral_level1": 1, + "auto_coral_level2": 1, + "auto_coral_level3": 1, + "auto_coral_level4": 1, + "teleop_coral_level1": 1, + "teleop_coral_level2": 1, + "teleop_coral_level3": 1, + "teleop_coral_level4": 1, + "auto_algae_net": 1, + "auto_algae_processor": 1, + "teleop_algae_net": 1, + "teleop_algae_processor": 1, + "climb_type": 1, + "climb_success": 1, + "defense_rating": 1, + "defense_notes": 1, + "auto_path": 1, + "auto_notes": 1, + "notes": 1, + "alliance": 1, + "scouter_id": 1, + "scouter_name": "$scouter.username", + "scouter_team": "$scouter.teamNumber", + "device_type": 1 } - ] + }) team_data = list(self.db.team_data.aggregate(pipeline)) return team_data @@ -230,22 +278,67 @@ def update_team_data(self, team_id, data, scouter_id): """Update existing team data if user is the owner""" self.ensure_connected() try: - # First verify ownership + # First verify ownership and get current data existing_data = self.db.team_data.find_one( - {"_id": ObjectId(team_id), "scouter_id": ObjectId(scouter_id)} + {"_id": ObjectId(team_id)} ) if not existing_data: - logger.warning( - f"Update attempted by non-owner scouter_id: {scouter_id}" - ) + logger.warning(f"Data not found for team_id: {team_id}") + return False + + # Check if the team is already scouted by someone else from the same team + pipeline = [ + { + "$match": { + "event_code": data["event_code"], + "match_number": int(data["match_number"]), + "team_number": int(data["team_number"]), + "_id": {"$ne": ObjectId(team_id)} # Exclude current entry + } + }, + { + "$lookup": { + "from": "users", + "localField": "scouter_id", + "foreignField": "_id", + "as": "scouter" + } + }, + {"$unwind": "$scouter"} + ] + + existing_entries = list(self.db.team_data.aggregate(pipeline)) + current_user = self.db.users.find_one({"_id": ObjectId(scouter_id)}) + + for entry in existing_entries: + if entry.get("scouter", {}).get("teamNumber") == current_user.get("teamNumber"): + logger.warning( + f"Update attempted for team {data['team_number']} in match {data['match_number']} " + f"which is already scouted by team {current_user.get('teamNumber')}" + ) + return False + + # Get match data to validate alliance sizes + match_data = list(self.db.team_data.find({ + "event_code": data["event_code"], + "match_number": int(data["match_number"]), + "_id": {"$ne": ObjectId(team_id)} # Exclude current entry + })) + + # Count teams per alliance + alliance = data.get("alliance", "red") + red_teams = [m for m in match_data if m["alliance"] == "red"] + blue_teams = [m for m in match_data if m["alliance"] == "blue"] + + if ((alliance == "red" and len(red_teams) >= 3) or (alliance == "blue" and len(blue_teams) >= 3)) and existing_data.get("alliance") != alliance: return False updated_data = { "team_number": int(data["team_number"]), "event_code": data["event_code"], "match_number": int(data["match_number"]), - "alliance": data.get("alliance", "red"), + "alliance": alliance, # Coral scoring "coral_level1": int(data.get("coral_level1", 0)), @@ -295,7 +388,7 @@ def update_team_data(self, team_id, data, scouter_id): } result = self.db.team_data.update_one( - {"_id": ObjectId(team_id), "scouter_id": ObjectId(scouter_id)}, + {"_id": ObjectId(team_id)}, {"$set": updated_data}, ) return result.modified_count > 0 @@ -434,87 +527,41 @@ def get_auto_paths(self, team_number): @with_mongodb_retry(retries=3, delay=2) def add_pit_scouting(self, data): - """Add new pit scouting data""" + """Add new pit scouting data with team validation""" self.ensure_connected() try: - if existing := self.db.pit_scouting.find_one( - { - "team_number": data["team_number"], - "scouter_id": data["scouter_id"], - } - ): - return False - - # Ensure required fields are present - pit_data = { - "team_number": int(data["team_number"]), - "scouter_id": ObjectId(data["scouter_id"]), - - # Drive base information - "drive_type": { - "swerve": data.get("drive_type", {}).get("swerve", False), - "tank": data.get("drive_type", {}).get("tank", False), - "other": data.get("drive_type", {}).get("other", "") - }, - "swerve_modules": data.get("swerve_modules", ""), - - # Motor details - "motor_details": { - "falcons": data.get("motor_details", {}).get("falcons", False), - "neos": data.get("motor_details", {}).get("neos", False), - "krakens": data.get("motor_details", {}).get("krakens", False), - "vortex": data.get("motor_details", {}).get("vortex", False), - "other": data.get("motor_details", {}).get("other", "") - }, - "motor_count": data.get("motor_count", 0), - - # Dimensions - "dimensions": { - "length": data.get("dimensions", {}).get("length", 0), - "width": data.get("dimensions", {}).get("width", 0), - "height": data.get("dimensions", {}).get("height", 0), - }, + team_number = int(data["team_number"]) + scouter_id = data["scouter_id"] - # Mechanisms - "mechanisms": { - "coral_scoring": { - "notes": data.get("mechanisms", {}).get("coral_scoring", {}).get("notes", "") - }, - "algae_scoring": { - "notes": data.get("mechanisms", {}).get("algae_scoring", {}).get("notes", "") - }, - "climber": { - "has_climber": data.get("mechanisms", {}).get("climber", {}).get("has_climber", False), - "type_climber": data.get("mechanisms", {}).get("climber", {}).get("type_climber", ""), - "notes": data.get("mechanisms", {}).get("climber", {}).get("notes", "") + # Check if this team is already scouted by someone from the same team + pipeline = [ + { + "$match": { + "team_number": team_number } }, - - # Programming and Autonomous - "programming_language": data.get("programming_language", ""), - "autonomous_capabilities": { - "has_auto": data.get("autonomous_capabilities", {}).get("has_auto", False), - "num_routes": data.get("autonomous_capabilities", {}).get("num_routes", 0), - "preferred_start": data.get("autonomous_capabilities", {}).get("preferred_start", ""), - "notes": data.get("autonomous_capabilities", {}).get("notes", "") - }, - - # Driver Experience - "driver_experience": { - "years": data.get("driver_experience", {}).get("years", 0), - "notes": data.get("driver_experience", {}).get("notes", "") + { + "$lookup": { + "from": "users", + "localField": "scouter_id", + "foreignField": "_id", + "as": "scouter" + } }, + {"$unwind": "$scouter"} + ] + + existing_entries = list(self.db.pit_scouting.aggregate(pipeline)) + current_user = self.db.users.find_one({"_id": ObjectId(scouter_id)}) + + for entry in existing_entries: + if entry.get("scouter", {}).get("teamNumber") == current_user.get("teamNumber"): + logger.warning(f"Team {team_number} has already been pit scouted by team {current_user.get('teamNumber')}") + return False - # Analysis - "notes": data.get("notes", ""), - - # Metadata - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc) - } + result = self.db.pit_scouting.insert_one(data) + return bool(result.inserted_id) - result = self.db.pit_scouting.insert_one(pit_data) - return result.inserted_id is not None except Exception as e: logger.error(f"Error adding pit scouting data: {str(e)}") return False @@ -572,9 +619,8 @@ def get_pit_scouting(self, team_number): return None @with_mongodb_retry(retries=3, delay=2) - def get_all_pit_scouting(self): - """Get all pit scouting data with scouter information""" - self.ensure_connected() + def get_all_pit_scouting(self, user_team_number=None, user_id=None): + """Get all pit scouting data with team-based access control""" try: pipeline = [ { @@ -585,68 +631,80 @@ def get_all_pit_scouting(self): "as": "scouter" } }, - { - "$unwind": { - "path": "$scouter", - "preserveNullAndEmptyArrays": True + {"$unwind": "$scouter"}, + ] + + # Add match stage for filtering based on team number or user ID + if user_team_number: + pipeline.append({ + "$match": { + "$or": [ + {"scouter.teamNumber": user_team_number}, + {"scouter._id": ObjectId(user_id)} + ] } - }, - { - "$project": { - "_id": 1, - "team_number": 1, - "drive_type": 1, - "swerve_modules": 1, - "motor_details": 1, - "motor_count": 1, - "dimensions": 1, - "mechanisms": 1, - "programming_language": 1, - "autonomous_capabilities": 1, - "driver_experience": 1, - "notes": 1, - "scouter_id": "$scouter._id", - "scouter_name": "$scouter.username", - "scouter_team": "$scouter.teamNumber" + }) + else: + pipeline.append({ + "$match": { + "scouter._id": ObjectId(user_id) } - } - ] - + }) + return list(self.db.pit_scouting.aggregate(pipeline)) except Exception as e: - logger.error(f"Error fetching all pit scouting data: {str(e)}") + logger.error(f"Error fetching pit scouting data: {str(e)}") return [] @with_mongodb_retry(retries=3, delay=2) def update_pit_scouting(self, team_number, data, scouter_id): - """Update pit scouting data""" + """Update pit scouting data with team validation""" self.ensure_connected() try: - # Use the same data structure as add_pit_scouting - pit_data = { - "team_number": int(team_number), - "scouter_id": ObjectId(scouter_id), - "drive_type": data.get("drive_type", {}), - "swerve_modules": data.get("swerve_modules", ""), - "motor_details": data.get("motor_details", {}), - "motor_count": data.get("motor_count", 0), - "dimensions": data.get("dimensions", {}), - "mechanisms": data.get("mechanisms", {}), - "programming_language": data.get("programming_language", ""), - "autonomous_capabilities": data.get("autonomous_capabilities", {}), - "driver_experience": data.get("driver_experience", {}), - "notes": data.get("notes", ""), - "updated_at": datetime.now(timezone.utc) - } - - result = self.db.pit_scouting.update_one( + # First verify ownership and get current data + existing_data = self.db.pit_scouting.find_one( + {"team_number": team_number} + ) + + if not existing_data: + logger.warning(f"Pit data not found for team: {team_number}") + return False + + # Check if this team is already scouted by someone else from the same team + pipeline = [ { - "team_number": int(team_number), - "scouter_id": ObjectId(scouter_id) + "$match": { + "team_number": team_number, + "_id": {"$ne": existing_data["_id"]} # Exclude current entry + } }, - {"$set": pit_data} + { + "$lookup": { + "from": "users", + "localField": "scouter_id", + "foreignField": "_id", + "as": "scouter" + } + }, + {"$unwind": "$scouter"} + ] + + existing_entries = list(self.db.pit_scouting.aggregate(pipeline)) + current_user = self.db.users.find_one({"_id": ObjectId(scouter_id)}) + + for entry in existing_entries: + if entry.get("scouter", {}).get("teamNumber") == current_user.get("teamNumber"): + logger.warning( + f"Update attempted for team {team_number} which is already pit scouted by team {current_user.get('teamNumber')}" + ) + return False + + result = self.db.pit_scouting.update_one( + {"team_number": team_number}, + {"$set": data} ) return result.modified_count > 0 + except Exception as e: logger.error(f"Error updating pit scouting data: {str(e)}") return False diff --git a/app/static/icons/icon-128x128.png b/app/static/icons/icon-128x128.png new file mode 100644 index 0000000..d2a9e45 Binary files /dev/null and b/app/static/icons/icon-128x128.png differ diff --git a/app/static/icons/icon-144x144.png b/app/static/icons/icon-144x144.png new file mode 100644 index 0000000..06f0c6b Binary files /dev/null and b/app/static/icons/icon-144x144.png differ diff --git a/app/static/icons/icon-152x152.png b/app/static/icons/icon-152x152.png new file mode 100644 index 0000000..c023600 Binary files /dev/null and b/app/static/icons/icon-152x152.png differ diff --git a/app/static/icons/icon-192x192.png b/app/static/icons/icon-192x192.png new file mode 100644 index 0000000..5acc6db Binary files /dev/null and b/app/static/icons/icon-192x192.png differ diff --git a/app/static/icons/icon-384x384.png b/app/static/icons/icon-384x384.png new file mode 100644 index 0000000..ffd0cc0 Binary files /dev/null and b/app/static/icons/icon-384x384.png differ diff --git a/app/static/icons/icon-512x512.png b/app/static/icons/icon-512x512.png new file mode 100644 index 0000000..55856d3 Binary files /dev/null and b/app/static/icons/icon-512x512.png differ diff --git a/app/static/icons/icon-72x72.png b/app/static/icons/icon-72x72.png new file mode 100644 index 0000000..93dbf04 Binary files /dev/null and b/app/static/icons/icon-72x72.png differ diff --git a/app/static/icons/icon-96x96.png b/app/static/icons/icon-96x96.png new file mode 100644 index 0000000..f717278 Binary files /dev/null and b/app/static/icons/icon-96x96.png differ diff --git a/app/static/js/compare.js b/app/static/js/compare.js index e23fb05..d8e9223 100644 --- a/app/static/js/compare.js +++ b/app/static/js/compare.js @@ -568,20 +568,94 @@ function displayRawData(teamsData) { }); } -// Make sure showAutoPath function is defined globally -window.showAutoPath = function(pathData, autoNotes = '') { +let modalCanvas, modalCoordSystem; +let currentPathData = null; + +// Update the showAutoPath function +function showAutoPath(pathData, autoNotes = '') { + currentPathData = pathData; + const modal = document.getElementById('autoPathModal'); - const image = document.getElementById('modalAutoPathImage'); - const notes = document.getElementById('modalAutoNotes'); + modal.classList.remove('hidden'); + + if (!modalCanvas) { + modalCanvas = document.getElementById('modalAutoPathCanvas'); + modalCoordSystem = new CanvasCoordinateSystem(modalCanvas); + + resizeModalCanvas(); + window.addEventListener('resize', resizeModalCanvas); + } + + redrawPaths(); - if (modal && image && notes) { - image.src = pathData; - notes.textContent = autoNotes || 'No auto notes provided'; - modal.classList.remove('hidden'); - } else { - console.error('Modal elements not found'); + const notesElement = document.getElementById('modalAutoNotes'); + if (notesElement) { + notesElement.textContent = autoNotes || 'No notes available'; + } +} + +function resizeModalCanvas() { + const container = modalCanvas.parentElement; + modalCanvas.width = container.clientWidth; + modalCanvas.height = container.clientHeight; + modalCoordSystem.updateTransform(); + redrawPaths(); +} + +function redrawPaths() { + if (!modalCoordSystem || !currentPathData) return; + + modalCoordSystem.clear(); + + let paths = currentPathData; + if (typeof paths === 'string') { + try { + paths = JSON.parse(paths); + } catch (e) { + console.error('Failed to parse path data:', e); + return; + } + } + + if (Array.isArray(paths)) { + paths.forEach(path => { + if (Array.isArray(path) && path.length > 0) { + const formattedPath = path.map(point => { + if (typeof point === 'object' && 'x' in point && 'y' in point) { + return { + x: (point.x / 1000) * modalCanvas.width, + y: (point.y / 300) * modalCanvas.height + }; + } + return null; + }).filter(point => point !== null); + + if (formattedPath.length > 0) { + modalCoordSystem.drawPath(formattedPath, '#3b82f6', 3); + } + } + }); + } +} + +function closeAutoPathModal() { + const modal = document.getElementById('autoPathModal'); + if (modal) { + modal.classList.add('hidden'); + } +} + +// Initialize modal click handler +window.addEventListener('load', function() { + const modal = document.getElementById('autoPathModal'); + if (modal) { + modal.addEventListener('click', function(e) { + if (e.target === modal) { + closeAutoPathModal(); + } + }); } -}; +}); function createRadarChart(data) { // Extract teams for comparison @@ -658,42 +732,4 @@ function getTeamColor(index) { '#FF9F40' ]; return colors[index % colors.length]; -} - -function showAutoPath(pathData, autoNotes = '') { - const modal = document.getElementById('autoPathModal'); - const image = document.getElementById('modalAutoPathImage'); - const notes = document.getElementById('modalAutoNotes'); - - image.src = pathData; - notes.textContent = autoNotes || 'No auto notes provided'; - modal.classList.remove('hidden'); -} - -function closeAutoPathModal() { - document.getElementById('autoPathModal').classList.add('hidden'); -} - -document.addEventListener('DOMContentLoaded', function() { - // ... existing initialization code ... - - // Add modal event listeners - const modal = document.getElementById('autoPathModal'); - if (modal) { - // Close on clicking outside the modal - modal.addEventListener('click', function(e) { - if (e.target === this) { - closeAutoPathModal(); - } - }); - - // Close on Escape key - document.addEventListener('keydown', function(e) { - if (e.key === 'Escape') { - closeAutoPathModal(); - } - }); - } - - // ... rest of your initialization code ... -}); \ No newline at end of file +} \ No newline at end of file diff --git a/app/static/js/scout.add.js b/app/static/js/scout.add.js index a8f4597..9d37197 100644 --- a/app/static/js/scout.add.js +++ b/app/static/js/scout.add.js @@ -59,7 +59,9 @@ function startDrawing(e) { } function draw(e) { - if (!isDrawing) return; + if (!isDrawing) { + return; + } e.preventDefault(); const point = getPointFromEvent(e); currentPath.push(point); @@ -67,7 +69,9 @@ function draw(e) { } function stopDrawing(e) { - if (!isDrawing) return; + if (!isDrawing) { + return; + } e.preventDefault(); isDrawing = false; if (currentPath.length > 1) { @@ -123,7 +127,7 @@ function redrawPaths() { function updateHiddenInput() { const input = document.getElementById('auto_path'); - input.value = JSON.stringify(paths); + input.value = paths.length > 0 ? JSON.stringify(paths) : JSON.stringify([]); } function undoLastPath() { @@ -145,7 +149,9 @@ function resetZoom() { } function zoomIn(event) { - if (!coordSystem) return; + if (!coordSystem) { + return; + } const rect = canvas.getBoundingClientRect(); let mouseX, mouseY; @@ -165,7 +171,9 @@ function zoomIn(event) { } function zoomOut(event) { - if (!coordSystem) return; + if (!coordSystem) { + return; + } const rect = canvas.getBoundingClientRect(); let mouseX, mouseY; @@ -210,6 +218,12 @@ document.addEventListener('DOMContentLoaded', function() { const eventCode = form.querySelector('input[name="event_code"]').value; const matchNumber = form.querySelector('input[name="match_number"]').value; + // Initialize auto_path with empty array if not set + const autoPathInput = form.querySelector('input[name="auto_path"]'); + if (!autoPathInput.value) { + autoPathInput.value = JSON.stringify([]); + } + try { const response = await fetch(`/scouting/check_team?team=${teamNumber}&event=${eventCode}&match=${matchNumber}`); const data = await response.json(); diff --git a/app/static/js/search.js b/app/static/js/search.js index 4ff9b07..f7e1aff 100644 --- a/app/static/js/search.js +++ b/app/static/js/search.js @@ -1,21 +1,91 @@ -// Define showAutoPath globally +let modalCanvas, modalCoordSystem; +let currentPathData = null; +let searchInput; +let selectedTeamInfo; + +function initializeCanvas() { + modalCanvas = document.getElementById('modalAutoPathCanvas'); + if (modalCanvas) { + modalCanvas.width = 500; + modalCanvas.height = 500; + modalCoordSystem = new CanvasCoordinateSystem(modalCanvas); + window.addEventListener('resize', resizeModalCanvas); + } +} + function showAutoPath(pathData, autoNotes = '') { - const modal = document.getElementById('autoPathModal'); - const image = document.getElementById('modalAutoPathImage'); - const notes = document.getElementById('modalAutoNotes'); + currentPathData = pathData; - image.src = pathData; - notes.textContent = autoNotes || 'No auto notes provided'; + const modal = document.getElementById('autoPathModal'); modal.classList.remove('hidden'); + + if (!modalCanvas) { + modalCanvas = document.getElementById('modalAutoPathCanvas'); + modalCoordSystem = new CanvasCoordinateSystem(modalCanvas); + + resizeModalCanvas(); + window.addEventListener('resize', resizeModalCanvas); + } + + redrawPaths(); + + const notesElement = document.getElementById('modalAutoNotes'); + if (notesElement) { + notesElement.textContent = autoNotes || 'No notes available'; + } } -function closeAutoPathModal() { - document.getElementById('autoPathModal').classList.add('hidden'); +function resizeModalCanvas() { + const container = modalCanvas.parentElement; + modalCanvas.width = container.clientWidth; + modalCanvas.height = container.clientHeight; + modalCoordSystem.updateTransform(); + redrawPaths(); } -let debounceTimer; -let searchInput; -let selectedTeamInfo; +function redrawPaths() { + if (!modalCoordSystem || !currentPathData) return; + + modalCoordSystem.clear(); + + let paths = currentPathData; + if (typeof currentPathData === 'string') { + try { + paths = JSON.parse(currentPathData); + } catch (e) { + console.error('Error parsing path data:', e); + return; + } + } + + if (Array.isArray(paths)) { + paths.forEach(path => { + if (Array.isArray(path) && path.length > 0) { + const formattedPath = path.map(point => { + if (typeof point === 'object' && 'x' in point && 'y' in point) { + return { + x: (point.x / 1000) * modalCanvas.width, + y: (point.y / 300) * modalCanvas.height + }; + } + return null; + }).filter(point => point !== null); + + if (formattedPath.length > 0) { + modalCoordSystem.drawPath(formattedPath, '#3b82f6', 3); + } + } + }); + } +} + +function closeAutoPathModal() { + const modal = document.getElementById('autoPathModal'); + modal.classList.add('hidden'); + if (modalCoordSystem) { + modalCoordSystem.resetView(); + } +} const init = (inputElement, teamInfoElement) => { if (!inputElement || !teamInfoElement) { @@ -60,6 +130,28 @@ const performSearch = async (query) => { } }; +const createPathCell = (entry) => { + const pathCell = document.createElement('td'); + pathCell.className = 'px-6 py-4 whitespace-nowrap'; + + if (entry.auto_path && entry.auto_path.length > 0) { + const pathButton = document.createElement('button'); + pathButton.className = 'text-blue-600 hover:text-blue-900'; + pathButton.textContent = 'View Path'; + pathButton.addEventListener('click', () => { + showAutoPath(entry.auto_path, entry.auto_notes); + }); + pathCell.appendChild(pathButton); + } else { + const noPath = document.createElement('span'); + noPath.className = 'text-gray-400'; + noPath.textContent = 'No path'; + pathCell.appendChild(noPath); + } + + return pathCell; +}; + const displayTeamInfo = (team) => { // Update basic team info document.getElementById('team-number').textContent = team.team_number; @@ -113,24 +205,8 @@ const displayTeamInfo = (team) => { climbSpan.textContent = `${entry.climb_success ? '✓' : '✗'} ${entry.climb_type || 'Failed'}`; // Create auto path cell - const pathCell = document.createElement('td'); - pathCell.className = 'px-6 py-4 whitespace-nowrap'; - if (entry.auto_path) { - const pathButton = document.createElement('button'); - pathButton.className = 'text-blue-600 hover:text-blue-900'; - pathButton.textContent = 'View Path'; - pathButton.addEventListener('click', () => { - showAutoPath(entry.auto_path, entry.auto_notes); - }); - pathCell.appendChild(pathButton); - } else { - const noPath = document.createElement('span'); - noPath.className = 'text-gray-400'; - noPath.textContent = 'No path'; - pathCell.appendChild(noPath); - } + const pathCell = createPathCell(entry); - // Add all cells to the row row.append( createCell(entry.event_code), createCell(entry.match_number), @@ -160,9 +236,23 @@ const displayTeamInfo = (team) => { selectedTeamInfo.classList.remove('hidden'); }; +// Initialize search on page load document.addEventListener('DOMContentLoaded', function() { - const searchInput = document.querySelector('#team-search'); - const selectedTeamInfo = document.querySelector('#selected-team-info'); - + const searchInput = document.getElementById('team-search'); + const selectedTeamInfo = document.getElementById('selected-team-info'); init(searchInput, selectedTeamInfo); -}); \ No newline at end of file +}); + +// Initialize modal click handler +window.addEventListener('load', function() { + const modal = document.getElementById('autoPathModal'); + if (modal) { + modal.addEventListener('click', function(e) { + if (e.target === modal) { + closeAutoPathModal(); + } + }); + } +}); + +let debounceTimer; \ No newline at end of file diff --git a/app/static/js/service-worker.js b/app/static/js/service-worker.js new file mode 100644 index 0000000..2498b65 --- /dev/null +++ b/app/static/js/service-worker.js @@ -0,0 +1,53 @@ +const CACHE_NAME = 'scouting-app-v1'; +const ASSETS_TO_CACHE = [ + '/', + '/static/css/global.css', + '/static/css/index.css', + '/static/js/CanvasCoordinateSystem.js', + '/static/images/field-2025.png', + '/static/images/default_profile.png', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => cache.addAll(ASSETS_TO_CACHE)) + ); +}); + +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request) + .then((response) => { + if (response) { + return response; + } + return fetch(event.request) + .then((response) => { + if (!response || response.status !== 200 || response.type !== 'basic') { + return response; + } + const responseToCache = response.clone(); + caches.open(CACHE_NAME) + .then((cache) => { + cache.put(event.request, responseToCache); + }); + return response; + }); + }) + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); \ No newline at end of file diff --git a/app/static/manifest.json b/app/static/manifest.json new file mode 100644 index 0000000..c7efd8f --- /dev/null +++ b/app/static/manifest.json @@ -0,0 +1,52 @@ +{ + "name": "Castle Scouting App", + "short_name": "Castle", + "description": "FRC Team Scouting App", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#3b82f6", + + "icons": [ + { + "src": "/static/icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "/static/icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "/static/icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "/static/icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "/static/icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "/static/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/static/icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "/static/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index fe1130a..b6e4609 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -52,6 +52,12 @@ + + + + + + @@ -281,6 +287,18 @@ } }); } + + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/static/js/service-worker.js') + .then((registration) => { + console.log('ServiceWorker registration successful'); + }) + .catch((err) => { + console.log('ServiceWorker registration failed: ', err); + }); + }); + } \ No newline at end of file diff --git a/app/templates/compare.html b/app/templates/compare.html index 7e8b179..e6bb068 100644 --- a/app/templates/compare.html +++ b/app/templates/compare.html @@ -433,27 +433,33 @@

Raw Scouting Data

-