import time def initialize_session_state(session): """Call this in the first app's creating_session method""" if 'participant_states' not in session.vars: session.vars['participant_states'] = {} session.vars['session_phase'] = 'soft_join' session.vars['max_participants'] = 3 def update_participant_state(session, participant_code, app_name, status='active'): """Update participant's current app and status""" states = session.vars.get('participant_states', {}) states[participant_code] = { 'current_app': app_name, 'status': status, 'last_update': time.time() } session.vars['participant_states'] = states def get_active_participant_count(session): """Count participants with status 'active'""" states = session.vars.get('participant_states', {}) return len([s for s in states.values() if s['status'] == 'active']) def check_session_full(session): """Check if session has reached max capacity""" return get_active_participant_count(session) >= session.vars.get('max_participants', 3) def remove_participant(session, participant_code): """Mark participant as dropped""" states = session.vars.get('participant_states', {}) if participant_code in states: states[participant_code]['status'] = 'dropped' session.vars['participant_states'] = states def get_participants_in_app(session, app_name): """Get list of participants currently in specific app""" states = session.vars.get('participant_states', {}) return [code for code, state in states.items() if state['status'] == 'active' and state['current_app'] == app_name] def all_participants_ready_for_commitment(session): """Check if all 3 participants have reached final instructions""" participants_in_final = get_participants_in_app(session, 'final_instructions') return len(participants_in_final) >= 3 def get_committed_count(session): """Count participants with status 'committed'""" states = session.vars.get('participant_states', {}) return len([s for s in states.values() if s['status'] == 'committed']) def all_participants_committed(session): """Check if all required participants have committed""" return get_committed_count(session) >= 3 def update_session_phase(session, new_phase): """Update the overall session phase""" session.vars['session_phase'] = new_phase def reset_session_to_recruiting(session): """Reset session back to recruiting after dropout during commitment""" session.vars['session_phase'] = 'soft_join' states = session.vars.get('participant_states', {}) for participant_code, state in states.items(): if state['status'] == 'dropped': del states[participant_code] session.vars['participant_states'] = states def get_session_status(session): """Get overview of session state for debugging/display""" return { 'phase': session.vars.get('session_phase', 'soft_join'), 'active_count': get_active_participant_count(session), 'committed_count': get_committed_count(session), 'participants': session.vars.get('participant_states', {}) } def cleanup_stale_participants(session, timeout_seconds=10): # Longer timeout """Remove participants who have been inactive for a longer time""" current_time = time.time() states = session.vars.get('participant_states', {}) participants_to_remove = [] for participant_code, state in states.items(): if state['status'] == 'active': last_update = state.get('last_update', 0) if current_time - last_update > timeout_seconds: participants_to_remove.append(participant_code) for participant_code in participants_to_remove: remove_participant(session, participant_code) return len(participants_to_remove) def is_participant_active_in_session(session, participant_code): """Check if a specific participant is currently active""" states = session.vars.get('participant_states', {}) participant_state = states.get(participant_code, {}) return participant_state.get('status') == 'active' def force_cleanup_if_full(session): """If session appears full, aggressively clean up ALL participants and start fresh""" if get_active_participant_count(session) >= 3: # Reset the entire session session.vars['participant_states'] = {} return True return False def try_add_participant_if_space(session, participant_code, app_name): """ Allow exactly 3 active participants, with replacement only for truly abandoned sessions """ import time print(f"\n=== PARTICIPANT {participant_code} TRYING TO JOIN {app_name} ===") if not hasattr(session.vars, 'adding_participant'): session.vars['adding_participant'] = False if session.vars.get('adding_participant', False): time.sleep(0.1) session.vars['adding_participant'] = True try: states = session.vars.get('participant_states', {}) current_time = time.time() # Only remove participants who have been completely inactive for 5+ minutes # This catches truly abandoned sessions, not people filling out forms participants_to_remove = [] for code, state in states.items(): if state['status'] == 'active': time_diff = current_time - state.get('last_update', 0) if time_diff > 60: # 5 minutes - much longer timeout participants_to_remove.append(code) print(f"CLEANUP: Removing truly abandoned participant {code} (inactive {time_diff:.1f}s)") for code in participants_to_remove: del states[code] session.vars['participant_states'] = states # If participant already exists, update them if participant_code in states: if states[participant_code].get('status') == 'blocked': print(f"❌ PARTICIPANT {participant_code} IS BLOCKED") return False else: # Update timestamp when they move between pages states[participant_code] = { 'current_app': app_name, 'status': 'active', 'last_update': current_time } session.vars['participant_states'] = states print(f"✅ UPDATED EXISTING PARTICIPANT {participant_code}") return True # Count active participants active_participants = [code for code, state in states.items() if state['status'] == 'active'] print(f"Active participants: {active_participants} (count: {len(active_participants)})") if len(active_participants) >= 3: states[participant_code] = { 'current_app': app_name, 'status': 'blocked', 'last_update': current_time } session.vars['participant_states'] = states print(f"❌ BLOCKED {participant_code} - 3 ACTIVE PARTICIPANTS") return False # Add new participant states[participant_code] = { 'current_app': app_name, 'status': 'active', 'last_update': current_time } session.vars['participant_states'] = states print(f"✅ ADDED {participant_code} - NOW HAVE {len(active_participants) + 1} ACTIVE") return True finally: session.vars['adding_participant'] = False def cleanup_and_reset_blocked_participants(session): import time current_time = time.time() states = session.vars.get('participant_states', {}) if not states: return 0 participants_to_remove = [] for participant_code, state in states.items(): if state['status'] == 'active': last_update = state.get('last_update', 0) time_diff = current_time - last_update # 30 seconds timeout for testing - reasonable but not too long if time_diff > 30: participants_to_remove.append(participant_code) print(f"CLEANUP: Removing {participant_code} (inactive {time_diff:.1f}s)") for participant_code in participants_to_remove: del states[participant_code] session.vars['participant_states'] = states return len(participants_to_remove) def get_committed_participants(session): """Return list of participant codes who are committed""" states = session.vars.get('participant_states', {}) return [code for code, state in states.items() if state['status'] == 'committed'] def cleanup_stale_participants_with_timeout(session, timeout_seconds=180): # Changed from 60 to 45 """Clean up participants who have been inactive for specified timeout""" import time current_time = time.time() states = session.vars.get('participant_states', {}) participants_to_remove = [] for code, state in states.items(): if state['status'] == 'active': time_diff = current_time - state.get('last_update', 0) if time_diff > timeout_seconds: participants_to_remove.append(code) for code in participants_to_remove: del states[code] session.vars['participant_states'] = states return len(participants_to_remove)