From e66c8ec291ce3980369bb27cdb6c04fb7aafe28c Mon Sep 17 00:00:00 2001 From: kcar Date: Fri, 29 May 2026 19:51:32 +0100 Subject: [PATCH] t4: consolidate seed scripts, remove demo modes, standardize passwords --- docker-entrypoint.sh | 26 +- init-production.sh | 2 +- main.py | 59 +-- run/initialization/__init__.py | 42 +- run/initialization/demo_school.py | 181 -------- run/initialization/demo_users.py | 218 ---------- run/initialization/seed_environment.py | 123 ++++-- .../seed_greenfield_timetable.py | 43 +- run/initialization/seed_test_environment.py | 409 +----------------- start.sh | 84 ++-- 10 files changed, 192 insertions(+), 995 deletions(-) delete mode 100644 run/initialization/demo_school.py delete mode 100644 run/initialization/demo_users.py diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index bf0af46..fd2f367 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -29,7 +29,7 @@ print_error() { # Check if we should run initialization RUN_INIT="${RUN_INIT:-false}" -INIT_MODE="${INIT_MODE:-infra}" # Default to 'infra', can be 'infra', 'full', or comma-separated list +INIT_MODE="${INIT_MODE:-infra}" # Default to 'infra', can be 'infra', 'seed', 'seed-test', 'full', or comma-separated list # If RUN_INIT is true, run initialization tasks if [ "$RUN_INIT" = "true" ]; then @@ -51,21 +51,21 @@ if [ "$RUN_INIT" = "true" ]; then } print_success "Infrastructure setup completed" ;; - "demo-school") - print_status "Creating demo school..." - python3 main.py --mode demo-school || { - print_error "Demo school creation failed!" + "seed") + print_status "Seeding canonical full environment..." + python3 main.py --mode seed || { + print_error "Seed failed!" exit 1 } - print_success "Demo school creation completed" + print_success "Seed completed" ;; - "demo-users") - print_status "Creating demo users..." - python3 main.py --mode demo-users || { - print_error "Demo users creation failed!" + "seed-test") + print_status "Seeding lightweight test environment..." + python3 main.py --mode seed-test || { + print_error "Seed test failed!" exit 1 } - print_success "Demo users creation completed" + print_success "Seed test completed" ;; "gais-data") print_status "Importing GAIS data..." @@ -78,9 +78,7 @@ if [ "$RUN_INIT" = "true" ]; then "full") print_status "Running full initialization..." python3 main.py --mode infra || exit 1 - python3 main.py --mode demo-school || exit 1 - python3 main.py --mode demo-users || exit 1 - python3 main.py --mode gais-data || exit 1 + python3 main.py --mode seed || exit 1 print_success "Full initialization completed" ;; *) diff --git a/init-production.sh b/init-production.sh index e689595..eca1ebe 100755 --- a/init-production.sh +++ b/init-production.sh @@ -1,7 +1,7 @@ #!/bin/bash # Helper script to run initialization tasks in production # Usage: ./init-production.sh [mode] -# Modes: infra, demo-school, demo-users, gais-data, full +# Modes: infra, seed, seed-test, gais-data, full set -e diff --git a/main.py b/main.py index a877544..58ef975 100644 --- a/main.py +++ b/main.py @@ -292,33 +292,20 @@ def run_infrastructure_mode(): logger.error(f"Infrastructure setup failed: {str(e)}") return False -def run_demo_school_mode(): - """Run demo school creation""" - logger.info("Running in demo school mode") - logger.info("Starting demo school creation...") - +def run_seed_mode(test: bool = False): + """Run canonical environment seed.""" + mode = "test" if test else "full" + logger.info(f"Running canonical seed mode ({mode})") try: - from run.initialization import initialize_demo_school_mode - initialize_demo_school_mode() - logger.info("Demo school creation completed successfully") - return True + from run.initialization.seed_environment import seed + import json + result = seed(test=test) + print(json.dumps(result, indent=2, default=str)) + return bool(result.get('success')) except Exception as e: - logger.error(f"Demo school creation failed: {str(e)}") + logger.error(f"Seed mode failed: {str(e)}") return False -def run_demo_users_mode(): - """Run demo users creation""" - logger.info("Running in demo users mode") - logger.info("Starting demo users creation...") - - try: - from run.initialization import initialize_demo_users_mode - initialize_demo_users_mode() - logger.info("Demo users creation completed successfully") - return True - except Exception as e: - logger.error(f"Demo users creation failed: {str(e)}") - return False def run_gais_data_mode(): """Run GAIS data import""" @@ -414,9 +401,8 @@ def parse_arguments(): epilog=""" Startup modes: infra - Setup infrastructure (Neo4j schema, calendar, Supabase buckets) - demo-school - Create demo school (KevlarAI) - demo-users - Create demo users - seed-test - Seed full test environment (2 schools, all test users) + seed - Seed canonical full environment (20 school users) + seed-test - Seed lightweight test environment (9 school users) gais-data - Import GAIS data (Edubase, etc.) dev - Run development server with auto-reload prod - Run production server (for Docker/containerized deployment) @@ -425,7 +411,7 @@ Startup modes: parser.add_argument( '--mode', '-m', - choices=['infra', 'demo-school', 'demo-users', 'seed-test', 'gais-data', 'dev', 'prod'], + choices=['infra', 'seed', 'seed-test', 'gais-data', 'dev', 'prod'], default='dev', help='Startup mode (default: dev)' ) @@ -448,22 +434,13 @@ if __name__ == "__main__": success = run_infrastructure_mode() sys.exit(0 if success else 1) - elif args.mode == 'demo-school': - # Run demo school creation - success = run_demo_school_mode() + elif args.mode == 'seed': + success = run_seed_mode(test=False) sys.exit(0 if success else 1) - - elif args.mode == 'demo-users': - # Run demo users creation - success = run_demo_users_mode() - sys.exit(0 if success else 1) - + elif args.mode == 'seed-test': - from run.initialization.seed_test_environment import seed_test_environment - import json - result = seed_test_environment() - print(json.dumps(result, indent=2)) - sys.exit(0 if result.get('success') else 1) + success = run_seed_mode(test=True) + sys.exit(0 if success else 1) elif args.mode == 'gais-data': # Run GAIS data import diff --git a/run/initialization/__init__.py b/run/initialization/__init__.py index caff1c2..102e9a0 100644 --- a/run/initialization/__init__.py +++ b/run/initialization/__init__.py @@ -1,6 +1,4 @@ from .infrastructure import initialize_infrastructure -from .demo_school import initialize_demo_school -from .demo_users import initialize_demo_users from .gais_data import import_gais_data from modules.logger_tool import initialise_logger import os @@ -10,54 +8,32 @@ logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH def initialize_infrastructure_mode() -> None: """Initialize infrastructure: Neo4j schema, calendar, and Supabase buckets""" logger.info("Starting infrastructure initialization...") - - # 1. Initialize Neo4j database, schema, and calendar structure + logger.info("Step 1: Initializing Neo4j infrastructure...") from .neo4j import initialize_neo4j neo4j_result = initialize_neo4j() - + if not neo4j_result["success"]: logger.error(f"Neo4j infrastructure initialization failed: {neo4j_result['message']}") return - - # 2. Initialize Supabase storage buckets + logger.info("Step 2: Initializing Supabase storage buckets...") from .buckets import initialize_buckets buckets_result = initialize_buckets() - + if not buckets_result["success"]: logger.error(f"Storage buckets initialization failed: {buckets_result['message']}") return - + logger.info("Infrastructure initialization completed successfully!") logger.info(f"Neo4j: {neo4j_result['message']}") logger.info(f"Buckets: {buckets_result['message']}") -def initialize_demo_school_mode() -> None: - """Initialize demo school (KevlarAI)""" - logger.info("Starting demo school initialization...") - result = initialize_demo_school() - - if result["success"]: - logger.info("Demo school initialization completed successfully") - else: - logger.error(f"Demo school initialization failed: {result['message']}") - -def initialize_demo_users_mode() -> None: - """Initialize demo users""" - logger.info("Starting demo users initialization...") - result = initialize_demo_users() - - if result["success"]: - logger.info("Demo users initialization completed successfully") - else: - logger.error(f"Demo users initialization failed: {result['message']}") - def initialize_gais_data_mode() -> None: """Initialize GAIS data import (Edubase, etc.)""" logger.info("Starting GAIS data import...") result = import_gais_data() - + if result["success"]: logger.info("GAIS data import completed successfully") else: @@ -65,11 +41,7 @@ def initialize_gais_data_mode() -> None: __all__ = [ 'initialize_infrastructure_mode', - 'initialize_demo_school_mode', - 'initialize_demo_users_mode', 'initialize_gais_data_mode', 'initialize_infrastructure', - 'initialize_demo_school', - 'initialize_demo_users', 'import_gais_data' -] \ No newline at end of file +] diff --git a/run/initialization/demo_school.py b/run/initialization/demo_school.py deleted file mode 100644 index 3993b9e..0000000 --- a/run/initialization/demo_school.py +++ /dev/null @@ -1,181 +0,0 @@ -""" -Demo school initialization module for ClassroomCopilot -Creates the KevlarAI demo school -""" -import os -import json -import requests -from typing import Dict, Any -from modules.logger_tool import initialise_logger -from modules.database.services.provisioning_service import ProvisioningService -import time - -logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) - -class DemoSchoolInitializer: - """Handles demo school creation""" - - def __init__(self, supabase_url: str, service_role_key: str): - self.supabase_url = supabase_url - self.service_role_key = service_role_key - self.supabase_headers = { - "apikey": service_role_key, - "Authorization": f"Bearer {service_role_key}", - "Content-Type": "application/json" - } - self.provisioning_service = ProvisioningService() - - def create_kevlarai_school(self) -> Dict[str, Any]: - """Create the KevlarAI demo school""" - logger.info("Creating KevlarAI demo school...") - - try: - # Check if KevlarAI school already exists - response = self._supabase_request_with_retry( - 'get', - f"{self.supabase_url}/rest/v1/institutes", - headers=self.supabase_headers, - params={ - "select": "*", - "name": "eq.KevlarAI" - } - ) - - if response.status_code == 200: - existing_schools = response.json() - if existing_schools and len(existing_schools) > 0: - logger.info("KevlarAI school already exists") - school = existing_schools[0] - try: - self.provisioning_service.ensure_school(school["id"]) - except Exception as provisioning_error: - logger.warning(f"Provisioning KevlarAI school failed: {provisioning_error}") - return { - "success": True, - "message": "KevlarAI school already exists", - "school": school - } - - # Create KevlarAI school - school_data = { - "name": "KevlarAI", - "urn": "KEVLARAI001", - "status": "active", - "address": { - "street": "123 Innovation Drive", - "town": "Tech City", - "county": "Digital County", - "postcode": "TC1 2AI", - "country": "United Kingdom" - }, - "website": "https://kevlar.ai", - "metadata": { - "school_type": "AI and Technology", - "phase_of_education": "Secondary and Further Education", - "establishment_status": "Open", - "specialization": "Artificial Intelligence, Machine Learning, Robotics" - } - } - - # Insert the school - response = self._supabase_request_with_retry('post', f"{self.supabase_url}/rest/v1/institutes", headers={**self.supabase_headers, "Prefer": "return=representation"}, json=school_data, params={"select": "*"}) - - logger.info(f"Supabase response status: {response.status_code}") - logger.info(f"Supabase response headers: {dict(response.headers)}") - logger.info(f"Supabase response text: {response.text}") - - if response.status_code in (200, 201): - try: - data = response.json() - school = data[0] if isinstance(data, list) and data else data - logger.info("Successfully created KevlarAI school") - # Ensure Neo4j provisioning is in place - try: - self.provisioning_service.ensure_school(school["id"]) - except Exception as provisioning_error: - logger.warning(f"Provisioning KevlarAI school failed: {provisioning_error}") - return { - "success": True, - "message": "Successfully created KevlarAI school", - "school": school - } - except json.JSONDecodeError as e: - logger.error(f"Failed to parse JSON response: {str(e)}") - logger.error(f"Response text: {response.text}") - # If the status code is successful but we can't parse JSON, - # the school was likely created successfully - return { - "success": True, - "message": "Successfully created KevlarAI school (response not JSON)", - "school": None - } - else: - logger.error(f"Failed to create KevlarAI school: {response.text}") - return { - "success": False, - "message": f"Failed to create KevlarAI school: {response.text}" - } - - except Exception as e: - logger.error(f"Error creating KevlarAI school: {str(e)}") - return { - "success": False, - "message": f"Error creating KevlarAI school: {str(e)}" - } - - def _supabase_request_with_retry(self, method, url, **kwargs): - """Make a request to Supabase with retry logic""" - max_retries = 3 - retry_delay = 2 # seconds - - for attempt in range(max_retries): - try: - if method.lower() == 'get': - response = requests.get(url, **kwargs) - elif method.lower() == 'post': - response = requests.post(url, **kwargs) - elif method.lower() == 'put': - response = requests.put(url, **kwargs) - elif method.lower() == 'delete': - response = requests.delete(url, **kwargs) - else: - raise ValueError(f"Unsupported HTTP method: {method}") - - # If successful or client error (4xx), don't retry - if response.status_code < 500: - return response - - # Server error (5xx), retry after delay - logger.warning(f"Supabase server error (attempt {attempt+1}/{max_retries}): {response.status_code} - {response.text}") - time.sleep(retry_delay * (attempt + 1)) # Exponential backoff - - except requests.RequestException as e: - logger.warning(f"Supabase request exception (attempt {attempt+1}/{max_retries}): {str(e)}") - if attempt == max_retries - 1: - raise - time.sleep(retry_delay * (attempt + 1)) - - # If we get here, all retries failed with server errors - raise requests.RequestException(f"Failed after {max_retries} attempts to {method} {url}") - -def initialize_demo_school() -> Dict[str, Any]: - """Initialize demo school (KevlarAI)""" - logger.info("Starting demo school initialization...") - - supabase_url = os.getenv("SUPABASE_URL") - service_role_key = os.getenv("SERVICE_ROLE_KEY") - - if not supabase_url or not service_role_key: - return {"success": False, "message": "Missing SUPABASE_URL or SERVICE_ROLE_KEY environment variables"} - - initializer = DemoSchoolInitializer(supabase_url, service_role_key) - - # Create KevlarAI school - result = initializer.create_kevlarai_school() - - if result["success"]: - logger.info("Demo school initialization completed successfully") - else: - logger.error(f"Demo school initialization failed: {result['message']}") - - return result diff --git a/run/initialization/demo_users.py b/run/initialization/demo_users.py deleted file mode 100644 index a20643f..0000000 --- a/run/initialization/demo_users.py +++ /dev/null @@ -1,218 +0,0 @@ -""" -Demo users initialization — creates the three canonical @kevlarai.com accounts -and links them to the KevlarAI institute in both Supabase and Neo4j. - -Idempotent: existing users are reused, stale .edu demo users are removed. -Run via: python3 main.py --mode demo-users -""" -import os -import requests -import time -from typing import Dict, Any -from modules.logger_tool import initialise_logger - -logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) - -INSTITUTE_DB = "cc.institutes.6585bf916ae84d72ab54cddf3ba4e648" -INSTITUTE_ID = "6585bf91-6ae8-4d72-ab54-cddf3ba4e648" - -DEMO_USERS = [ - { - "email": "kcar@kevlarai.com", - "password": "KevlarAI2025!", - "username": "kcar", - "full_name": "Kevin Carroll", - "display_name": "Kevin", - "user_type": "teacher", - "role": "school_admin", - }, - { - "email": "teacher1@kevlarai.com", - "password": "Teacher1@KevlarAI!", - "username": "teacher1.kevlarai", - "full_name": "Sarah Chen", - "display_name": "Sarah", - "user_type": "teacher", - "role": "teacher", - }, - { - "email": "teacher2@kevlarai.com", - "password": "Teacher2@KevlarAI!", - "username": "teacher2.kevlarai", - "full_name": "Marcus Rodriguez", - "display_name": "Marcus", - "user_type": "teacher", - "role": "teacher", - }, -] - - -def initialize_demo_users() -> Dict[str, Any]: - """Create/refresh canonical @kevlarai.com demo users.""" - from neo4j import GraphDatabase - from modules.database.supabase.utils.client import SupabaseServiceRoleClient - - sb_client = SupabaseServiceRoleClient() - supabase_url = os.environ["SUPABASE_URL"] - service_key = os.environ["SERVICE_ROLE_KEY"] - auth_headers = { - "apikey": service_key, - "Authorization": f"Bearer {service_key}", - "Content-Type": "application/json", - } - - def auth_get(path, params=None): - r = requests.get(f"{supabase_url}/auth/v1/admin{path}", headers=auth_headers, params=params) - r.raise_for_status() - return r.json() - - def auth_post(path, data): - return requests.post(f"{supabase_url}/auth/v1/admin{path}", headers=auth_headers, json=data) - - def auth_delete(path): - return requests.delete(f"{supabase_url}/auth/v1/admin{path}", headers=auth_headers) - - def sb_upsert(table, data, on_conflict=None): - params = {} - if on_conflict: - params["on_conflict"] = on_conflict - hdrs = {**auth_headers, "Prefer": "resolution=merge-duplicates,return=representation"} - r = requests.post(f"{supabase_url}/rest/v1/{table}", headers=hdrs, json=data, params=params) - return r - - errors = [] - - # ── Step 1: delete stale .edu users ───────────────────────────────────── - logger.info("Removing stale .edu demo users...") - try: - existing = auth_get("/users", params={"per_page": 100}).get("users", []) - edu_users = [u for u in existing if u.get("email", "").endswith("@kevlarai.edu")] - for u in edu_users: - # Try direct delete first - r = auth_delete(f"/users/{u['id']}") - if r.status_code not in (200, 204): - # Profile has dependent rows — clean via SQL - _purge_profile_rows(supabase_url, service_key, u["id"]) - auth_delete(f"/users/{u['id']}") - logger.info(f" Removed: {u['email']}") - except Exception as e: - logger.warning(f" .edu cleanup warning: {e}") - - # ── Step 2: create @kevlarai.com users ─────────────────────────────────── - logger.info("Creating @kevlarai.com demo users...") - created_users = {} - for spec in DEMO_USERS: - email = spec["email"] - all_users = auth_get("/users", params={"per_page": 100}).get("users", []) - existing_user = next((u for u in all_users if u.get("email") == email), None) - - if existing_user: - uid = existing_user["id"] - logger.info(f" {email}: already exists [{uid[:8]}]") - created_users[email] = {"id": uid, **spec} - continue - - r = auth_post("/users", { - "email": email, - "password": spec["password"], - "email_confirm": True, - "user_metadata": { - "username": spec["username"], - "full_name": spec["full_name"], - "display_name": spec["display_name"], - "user_type": spec["user_type"], - }, - }) - if r.status_code in (200, 201): - uid = r.json()["id"] - logger.info(f" {email}: created [{uid[:8]}]") - created_users[email] = {"id": uid, **spec} - else: - msg = f"Failed to create {email}: {r.text[:200]}" - logger.error(f" {msg}") - errors.append(msg) - time.sleep(0.3) - - # ── Step 3: upsert profiles ────────────────────────────────────────────── - logger.info("Upserting profiles...") - for spec in DEMO_USERS: - u = created_users.get(spec["email"]) - if not u: - continue - sb_upsert("profiles", { - "id": u["id"], - "email": spec["email"], - "user_type": spec["user_type"], - "username": spec["username"], - "full_name": spec["full_name"], - "display_name": spec["display_name"], - "school_id": INSTITUTE_ID, - "neo4j_sync_status": "pending", - }, on_conflict="id") - - # ── Step 4: upsert memberships ─────────────────────────────────────────── - logger.info("Upserting institute memberships...") - for spec in DEMO_USERS: - u = created_users.get(spec["email"]) - if not u: - continue - sb_upsert("institute_memberships", { - "profile_id": u["id"], - "institute_id": INSTITUTE_ID, - "role": spec["role"], - }, on_conflict="profile_id,institute_id") - - # ── Step 5: Teacher nodes in Neo4j ─────────────────────────────────────── - logger.info("Creating Neo4j Teacher nodes...") - try: - driver = GraphDatabase.driver("bolt://192.168.0.209:7687", auth=("neo4j", "&%N304j&%")) - new_emails = {spec["email"] for spec in DEMO_USERS} - with driver.session(database=INSTITUTE_DB) as s: - # Remove stale Teacher nodes - stale = s.run("MATCH (t:Teacher) WHERE NOT t.worker_email IN $emails RETURN t.worker_email as e, t.uuid_string as u", emails=list(new_emails)).data() - for t in stale: - s.run("MATCH (t:Teacher {uuid_string: $u}) DETACH DELETE t", u=t["u"]) - logger.info(f" Removed stale Teacher: {t['e']}") - # Remove duplicate Teacher nodes (same email, different UUID) - for spec in DEMO_USERS: - u = created_users.get(spec["email"]) - if not u: - continue - dupes = s.run("MATCH (t:Teacher {worker_email: $e}) WHERE t.uuid_string <> $u RETURN t.uuid_string as uid", e=spec["email"], u=u["id"]).data() - for d in dupes: - s.run("MATCH (t:Teacher {uuid_string: $u}) DETACH DELETE t", u=d["uid"]) - logger.info(f" Removed duplicate Teacher UUID {d['uid'][:8]} for {spec['email']}") - # Upsert Teacher nodes - for spec in DEMO_USERS: - u = created_users.get(spec["email"]) - if not u: - continue - s.run(""" - MERGE (t:Teacher {uuid_string: $uuid}) - SET t.worker_email = $email, - t.worker_name = $name, - t.unique_id = $uid, - t.user_type = 'teacher', - t.worker_type = 'teacher' - """, uuid=u["id"], email=spec["email"], name=spec["full_name"], uid=u["id"]) - logger.info(f" Teacher node: {spec['email']} [{u['id'][:8]}]") - driver.close() - except Exception as e: - msg = f"Neo4j Teacher node setup failed: {e}" - logger.error(msg) - errors.append(msg) - - return { - "success": len(errors) == 0, - "created": list(created_users.keys()), - "errors": errors, - "message": "Demo users initialized" if not errors else f"{len(errors)} errors: {errors[0]}", - } - - -def _purge_profile_rows(supabase_url: str, service_key: str, profile_id: str) -> None: - """Delete all rows referencing a profile before deleting the auth user.""" - hdrs = {"apikey": service_key, "Authorization": f"Bearer {service_key}"} - for table, col in [("files", "uploaded_by"), ("whiteboard_rooms", "user_id"), - ("cabinet_memberships", "profile_id"), ("institute_memberships", "profile_id")]: - requests.delete(f"{supabase_url}/rest/v1/{table}?{col}=eq.{profile_id}", headers=hdrs) diff --git a/run/initialization/seed_environment.py b/run/initialization/seed_environment.py index 0141acc..5055f19 100644 --- a/run/initialization/seed_environment.py +++ b/run/initialization/seed_environment.py @@ -23,7 +23,12 @@ Uniform accounts per school (10 × 2 = 20 total) student3@{domain} student Run from inside the ccapi container: + python3 main.py --mode seed + python3 main.py --mode seed-test + +Or directly: python3 -c "from run.initialization.seed_environment import seed; seed()" + python3 -c "from run.initialization.seed_environment import seed; seed(test=True)" """ import os import time @@ -49,27 +54,54 @@ GREENFIELD_DOMAIN = "greenfieldacademy.test" # ─── Passwords ──────────────────────────────────────────────────────────────── -PWD_ADMIN = "Admin@Cc2025!" -PWD_TEACHER = "Teacher@Cc2025!" -PWD_STUDENT = "Student@Cc2025!" +DEFAULT_PLATFORM_ADMIN_PASSWORD = "KevlarAI2025!" +DEFAULT_SCHOOL_ADMIN_PASSWORD = "Admin@Cc2025!" +DEFAULT_TEACHER_PASSWORD = "Teacher@Cc2025!" +DEFAULT_STUDENT_PASSWORD = "Student@Cc2025!" + + +def get_seed_password(role: str) -> str: + """Return the seed password for a role, allowing env overrides.""" + normalized = role.lower().strip() + env_by_role = { + "platform_admin": ("SEED_PLATFORM_ADMIN_PASSWORD", DEFAULT_PLATFORM_ADMIN_PASSWORD), + "school_admin": ("SEED_SCHOOL_ADMIN_PASSWORD", DEFAULT_SCHOOL_ADMIN_PASSWORD), + "teacher": ("SEED_TEACHER_PASSWORD", DEFAULT_TEACHER_PASSWORD), + "student": ("SEED_STUDENT_PASSWORD", DEFAULT_STUDENT_PASSWORD), + } + if normalized not in env_by_role: + raise ValueError(f"Unknown seed password role: {role}") + env_name, default = env_by_role[normalized] + return os.getenv(env_name, default) + + +def get_seed_passwords() -> Dict[str, str]: + return { + "platform_admin": get_seed_password("platform_admin"), + "school_admin": get_seed_password("school_admin"), + "teacher": get_seed_password("teacher"), + "student": get_seed_password("student"), + } + # ─── Account template ──────────────────────────────────────────────────────── def _school_accounts(domain: str, institute_id: str) -> List[Dict]: + passwords = get_seed_passwords() return [ # school_admin accounts { "prefix": "admin", "email": f"admin@{domain}", "full_name": "Alex Admin", "display_name": "Alex", "username": f"admin.{domain.replace('.', '_')}", - "user_type": "teacher", "role": "school_admin", "password": PWD_ADMIN, + "user_type": "teacher", "role": "school_admin", "password": passwords["school_admin"], "institute_id": institute_id, }, { "prefix": "head", "email": f"head@{domain}", "full_name": "Helen Head", "display_name": "Helen", "username": f"head.{domain.replace('.', '_')}", - "user_type": "teacher", "role": "school_admin", "password": PWD_ADMIN, + "user_type": "teacher", "role": "school_admin", "password": passwords["school_admin"], "institute_id": institute_id, }, # teacher accounts @@ -77,35 +109,35 @@ def _school_accounts(domain: str, institute_id: str) -> List[Dict]: "prefix": "physics", "email": f"physics@{domain}", "full_name": "Phil Physics", "display_name": "Phil", "username": f"physics.{domain.replace('.', '_')}", - "user_type": "teacher", "role": "teacher", "password": PWD_TEACHER, + "user_type": "teacher", "role": "teacher", "password": passwords["teacher"], "institute_id": institute_id, }, { "prefix": "maths", "email": f"maths@{domain}", "full_name": "Mary Maths", "display_name": "Mary", "username": f"maths.{domain.replace('.', '_')}", - "user_type": "teacher", "role": "teacher", "password": PWD_TEACHER, + "user_type": "teacher", "role": "teacher", "password": passwords["teacher"], "institute_id": institute_id, }, { "prefix": "teacher1", "email": f"teacher1@{domain}", "full_name": "Tom Teacher", "display_name": "Tom", "username": f"teacher1.{domain.replace('.', '_')}", - "user_type": "teacher", "role": "teacher", "password": PWD_TEACHER, + "user_type": "teacher", "role": "teacher", "password": passwords["teacher"], "institute_id": institute_id, }, { "prefix": "teacher2", "email": f"teacher2@{domain}", "full_name": "Tara Teach", "display_name": "Tara", "username": f"teacher2.{domain.replace('.', '_')}", - "user_type": "teacher", "role": "teacher", "password": PWD_TEACHER, + "user_type": "teacher", "role": "teacher", "password": passwords["teacher"], "institute_id": institute_id, }, { "prefix": "teacher3", "email": f"teacher3@{domain}", "full_name": "Tim Teachwell", "display_name": "Tim", "username": f"teacher3.{domain.replace('.', '_')}", - "user_type": "teacher", "role": "teacher", "password": PWD_TEACHER, + "user_type": "teacher", "role": "teacher", "password": passwords["teacher"], "institute_id": institute_id, }, # student accounts @@ -113,30 +145,49 @@ def _school_accounts(domain: str, institute_id: str) -> List[Dict]: "prefix": "student1", "email": f"student1@{domain}", "full_name": "Sam Student", "display_name": "Sam", "username": f"student1.{domain.replace('.', '_')}", - "user_type": "student", "role": "student", "password": PWD_STUDENT, + "user_type": "student", "role": "student", "password": passwords["student"], "institute_id": institute_id, }, { "prefix": "student2", "email": f"student2@{domain}", "full_name": "Sophie Study", "display_name": "Sophie", "username": f"student2.{domain.replace('.', '_')}", - "user_type": "student", "role": "student", "password": PWD_STUDENT, + "user_type": "student", "role": "student", "password": passwords["student"], "institute_id": institute_id, }, { "prefix": "student3", "email": f"student3@{domain}", "full_name": "Steve Scholar", "display_name": "Steve", "username": f"student3.{domain.replace('.', '_')}", - "user_type": "student", "role": "student", "password": PWD_STUDENT, + "user_type": "student", "role": "student", "password": passwords["student"], "institute_id": institute_id, }, ] -ALL_ACCOUNTS = ( +FULL_ACCOUNTS = ( _school_accounts(KEVLARAI_DOMAIN, KEVLARAI_ID) + _school_accounts(GREENFIELD_DOMAIN, GREENFIELD_ID) ) + +def get_accounts(test: bool = False) -> List[Dict]: + """Return full (20-user) or lightweight test (9-user) seed fixtures.""" + if not test: + return list(FULL_ACCOUNTS) + + wanted = { + f"student1@{KEVLARAI_DOMAIN}", + f"student2@{KEVLARAI_DOMAIN}", + f"student3@{KEVLARAI_DOMAIN}", + f"admin@{GREENFIELD_DOMAIN}", + f"physics@{GREENFIELD_DOMAIN}", + f"maths@{GREENFIELD_DOMAIN}", + f"teacher1@{GREENFIELD_DOMAIN}", + f"student1@{GREENFIELD_DOMAIN}", + f"student2@{GREENFIELD_DOMAIN}", + } + return [account for account in FULL_ACCOUNTS if account["email"] in wanted] + # ─── Supabase helpers ───────────────────────────────────────────────────────── def _sb_ctx(): @@ -183,18 +234,19 @@ def _rest_patch(url, headers, table, match_col, match_val, data): # ─── Main seed function ─────────────────────────────────────────────────────── -def seed() -> Dict[str, Any]: +def seed(test: bool = False) -> Dict[str, Any]: from modules.database.services.provisioning_service import ProvisioningService from modules.database.services.neo4j_service import Neo4jService from modules.database.init.init_calendar import create_calendar + accounts = get_accounts(test=test) url, headers = _sb_ctx() errors: List[str] = [] - results: Dict[str, Any] = {} + results: Dict[str, Any] = {"mode": "test" if test else "full", "account_count": len(accounts)} # ── Step 1: Fix KevlarAI institute record ───────────────────────────────── logger.info("=" * 60) - logger.info("SEED ENVIRONMENT") + logger.info(f"SEED ENVIRONMENT ({'test' if test else 'full'} mode)") logger.info("=" * 60) logger.info("\n[1] KevlarAI institute record...") try: @@ -272,7 +324,7 @@ def seed() -> Dict[str, Any]: results["global_calendar"] = "error" # ── Step 5: Create / verify auth users ──────────────────────────────────── - logger.info("[5] Creating auth users (20 accounts)...") + logger.info(f"[5] Creating auth users ({len(accounts)} accounts)...") try: existing = _auth_get(url, headers, "/users", {"per_page": 200}).get("users", []) existing_by_email = {u["email"]: u for u in existing} @@ -281,7 +333,7 @@ def seed() -> Dict[str, Any]: existing_by_email = {} created_users: Dict[str, str] = {} # email → uid - for spec in ALL_ACCOUNTS: + for spec in accounts: email = spec["email"] if email in existing_by_email: created_users[email] = existing_by_email[email]["id"] @@ -311,7 +363,7 @@ def seed() -> Dict[str, Any]: # ── Step 6: Upsert profiles and memberships ─────────────────────────────── logger.info("[6] Upserting profiles and memberships...") - for spec in ALL_ACCOUNTS: + for spec in accounts: uid = created_users.get(spec["email"]) if not uid: continue @@ -341,12 +393,18 @@ def seed() -> Dict[str, Any]: # ── Step 7: Merge Neo4j Teacher/Student nodes ───────────────────────────── logger.info("[7] Merging Neo4j worker nodes...") try: - from neo4j import GraphDatabase - driver = GraphDatabase.driver("bolt://192.168.0.209:7687", auth=("neo4j", "&%N304j&%")) + from modules.database.tools.neo4j_driver_tools import close_driver, get_driver + bolt_url = os.getenv("NEO4J_BOLT_URL") or os.getenv("APP_BOLT_URL") + neo4j_user = os.getenv("NEO4J_USER") or os.getenv("USER_NEO4J") + neo4j_password = os.getenv("NEO4J_PASSWORD") or os.getenv("PASSWORD_NEO4J") + auth = (neo4j_user, neo4j_password) if neo4j_user and neo4j_password else None + driver = get_driver(url=bolt_url, auth=auth) if bolt_url else get_driver() + if driver is None: + raise RuntimeError("Neo4j driver unavailable; check NEO4J_BOLT_URL/APP_BOLT_URL and NEO4J_PASSWORD/PASSWORD_NEO4J") # Group by institute DB by_db: Dict[str, List[Dict]] = {} - for spec in ALL_ACCOUNTS: + for spec in accounts: uid = created_users.get(spec["email"]) if not uid: continue @@ -369,7 +427,7 @@ def seed() -> Dict[str, Any]: ) logger.info(f" [{db[:35]}] {len(users)} nodes merged ✓") - driver.close() + close_driver(driver) results["neo4j_nodes"] = "ok" except Exception as e: errors.append(f"neo4j_nodes: {e}") @@ -427,7 +485,7 @@ def seed() -> Dict[str, Any]: results["success"] = len(errors) == 0 results["errors"] = errors - _print_credential_sheet(created_users) + _print_credential_sheet(created_users, accounts) logger.info("\n" + "=" * 60) if errors: @@ -440,14 +498,16 @@ def seed() -> Dict[str, Any]: return results -def _print_credential_sheet(created_users: Dict[str, str]): +def _print_credential_sheet(created_users: Dict[str, str], accounts: List[Dict]): PAD = 36 + include_passwords = os.getenv("PRINT_SEED_CREDENTIALS", "").lower() in {"1", "true", "yes", "on"} logger.info("\n" + "=" * 70) - logger.info("CREDENTIAL SHEET") + logger.info("CREDENTIAL SHEET" + ("" if include_passwords else " (passwords redacted; set PRINT_SEED_CREDENTIALS=true to print)")) logger.info("=" * 70) logger.info(f" {'ROLE':<16} {'EMAIL':<{PAD}} PASSWORD") logger.info(f" {'-'*14} {'-'*(PAD-2)} -----------") - logger.info(f" {'[platform admin]':<16} {'kcar@kevlarai.com':<{PAD}} KevlarAI2025!") + platform_password = get_seed_password("platform_admin") if include_passwords else "" + logger.info(f" {'[platform admin]':<16} {'kcar@kevlarai.com':<{PAD}} {platform_password}") logger.info("") for school_id, domain, label in [ @@ -455,16 +515,17 @@ def _print_credential_sheet(created_users: Dict[str, str]): (GREENFIELD_ID, GREENFIELD_DOMAIN, "Greenfield Academy"), ]: logger.info(f" [{label}]") - for spec in ALL_ACCOUNTS: + for spec in accounts: if spec["institute_id"] != school_id: continue uid = created_users.get(spec["email"], "—") status = f"[{uid[:8]}]" if uid != "—" else "[MISSING]" - logger.info(f" {spec['role']:<16} {spec['email']:<{PAD}} {spec['password']} {status}") + password = spec["password"] if include_passwords else "" + logger.info(f" {spec['role']:<16} {spec['email']:<{PAD}} {password} {status}") logger.info("") logger.info("=" * 70) if __name__ == "__main__": import json - print(json.dumps(seed(), indent=2, default=str)) + print(json.dumps(seed(test="--test" in os.sys.argv), indent=2, default=str)) diff --git a/run/initialization/seed_greenfield_timetable.py b/run/initialization/seed_greenfield_timetable.py index 6d1968e..07b5c7f 100644 --- a/run/initialization/seed_greenfield_timetable.py +++ b/run/initialization/seed_greenfield_timetable.py @@ -20,14 +20,17 @@ import time import requests from typing import Dict, Any, Optional, List -SUPA_URL = os.environ["SUPABASE_URL"] -SERVICE_KEY = os.environ["SERVICE_ROLE_KEY"] -API_BASE = os.environ.get("API_BASE_URL", "http://localhost:8000") +from run.initialization.seed_environment import get_seed_password GREENFIELD_ADMIN_EMAIL = "admin@greenfieldacademy.test" -GREENFIELD_ADMIN_PWD = "Admin@Cc2025!" -PWD_TEACHER = "Teacher@Cc2025!" -PWD_STUDENT = "Student@Cc2025!" + + +def _runtime_context() -> Dict[str, str]: + return { + "supa_url": os.environ["SUPABASE_URL"], + "service_key": os.environ["SERVICE_ROLE_KEY"], + "api_base": os.environ.get("API_BASE_URL", "http://localhost:8000"), + } # ─── Period templates ────────────────────────────────────────────────────────── @@ -145,18 +148,20 @@ STUDENT_ENROLLMENTS = { # ─── Helpers ─────────────────────────────────────────────────────────────────── def _sb_headers() -> Dict: + service_key = _runtime_context()["service_key"] return { - "apikey": SERVICE_KEY, - "Authorization": f"Bearer {SERVICE_KEY}", + "apikey": service_key, + "Authorization": f"Bearer {service_key}", "Content-Type": "application/json", "Prefer": "return=representation", } def _sign_in(email: str, password: str) -> str: + ctx = _runtime_context() r = requests.post( - f"{SUPA_URL}/auth/v1/token?grant_type=password", - headers={"apikey": SERVICE_KEY, "Content-Type": "application/json"}, + f"{ctx['supa_url']}/auth/v1/token?grant_type=password", + headers={"apikey": ctx["service_key"], "Content-Type": "application/json"}, json={"email": email, "password": password}, ) r.raise_for_status() @@ -164,8 +169,9 @@ def _sign_in(email: str, password: str) -> str: def _api(token: str, method: str, path: str, body: Dict = None) -> Dict: + api_base = _runtime_context()["api_base"] h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} - r = getattr(requests, method)(f"{API_BASE}{path}", headers=h, json=body) + r = getattr(requests, method)(f"{api_base}{path}", headers=h, json=body) try: return r.json() except Exception: @@ -174,8 +180,9 @@ def _api(token: str, method: str, path: str, body: Dict = None) -> Dict: def _get_profile_id(email: str) -> Optional[str]: """Look up a profile's UUID by email via Supabase service role.""" + supa_url = _runtime_context()["supa_url"] r = requests.get( - f"{SUPA_URL}/rest/v1/profiles", + f"{supa_url}/rest/v1/profiles", headers=_sb_headers(), params={"email": f"eq.{email}", "select": "id", "limit": "1"}, ) @@ -185,8 +192,9 @@ def _get_profile_id(email: str) -> Optional[str]: def _get_teacher_timetable_id(profile_id: str) -> Optional[str]: """Return the Supabase teacher_timetables.id for a given profile.""" + supa_url = _runtime_context()["supa_url"] r = requests.get( - f"{SUPA_URL}/rest/v1/teacher_timetables", + f"{supa_url}/rest/v1/teacher_timetables", headers=_sb_headers(), params={"profile_id": f"eq.{profile_id}", "select": "id", "limit": "1"}, ) @@ -196,10 +204,11 @@ def _get_teacher_timetable_id(profile_id: str) -> Optional[str]: def _patch_slot_class_ids(teacher_tt_sb_id: str, class_code_to_id: Dict[str, str]) -> int: """Update class_id FK on teacher_timetable_slots rows via Supabase service role.""" + supa_url = _runtime_context()["supa_url"] patched = 0 for code, class_uuid in class_code_to_id.items(): r = requests.patch( - f"{SUPA_URL}/rest/v1/teacher_timetable_slots", + f"{supa_url}/rest/v1/teacher_timetable_slots", headers=_sb_headers(), params={ "teacher_timetable_id": f"eq.{teacher_tt_sb_id}", @@ -224,7 +233,7 @@ def seed() -> Dict[str, Any]: # ── [1] Sign in as Greenfield admin ─────────────────────────────────────── print("\n[1] Signing in as admin@greenfieldacademy.test...") try: - admin_token = _sign_in(GREENFIELD_ADMIN_EMAIL, GREENFIELD_ADMIN_PWD) + admin_token = _sign_in(GREENFIELD_ADMIN_EMAIL, get_seed_password("school_admin")) print(" ✓ signed in") except Exception as e: return {"success": False, "error": str(e)} @@ -331,7 +340,7 @@ def seed() -> Dict[str, Any]: for teacher_email, slot_tuples in TEACHER_SLOTS.items(): try: - teacher_token = _sign_in(teacher_email, PWD_TEACHER) + teacher_token = _sign_in(teacher_email, get_seed_password("teacher")) except Exception as e: err = f"login {teacher_email}: {e}" print(f" ✗ {err}") @@ -439,7 +448,7 @@ def seed() -> Dict[str, Any]: results["materialize"] = {} for teacher_email in TEACHER_SLOTS: try: - teacher_token = _sign_in(teacher_email, PWD_TEACHER) + teacher_token = _sign_in(teacher_email, get_seed_password("teacher")) except Exception as e: err = f"login {teacher_email}: {e}" print(f" ✗ {err}") diff --git a/run/initialization/seed_test_environment.py b/run/initialization/seed_test_environment.py index e3f83e6..24ce513 100644 --- a/run/initialization/seed_test_environment.py +++ b/run/initialization/seed_test_environment.py @@ -1,408 +1,15 @@ -""" -Seed Test Environment — idempotent full-environment setup for CC development. +"""Compatibility wrapper for the canonical seed environment test mode.""" +from typing import Any, Dict -Creates: - - kcar@kevlarai.com → platform super-admin (admin_profiles) - - KevlarAI school → already exists; adds 3 student users - - Greenfield Academy → new second school with full staff + students - -Run inside ccapi container: - python3 main.py --mode seed-test - -Or directly: - cd ~/api && python3 -c " - from run.initialization.seed_test_environment import seed_test_environment - import json; print(json.dumps(seed_test_environment(), indent=2)) - " -""" -import os -import time -import requests -import uuid -from typing import Dict, Any, Optional, List -from modules.logger_tool import initialise_logger - -logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), "default", True) - -# ─── Existing KevlarAI school ──────────────────────────────────────────────── -KEVLARAI_INSTITUTE_ID = "6585bf91-6ae8-4d72-ab54-cddf3ba4e648" -KEVLARAI_INSTITUTE_DB = "cc.institutes.6585bf916ae84d72ab54cddf3ba4e648" - -# ─── Second test school ────────────────────────────────────────────────────── -GREENFIELD_INSTITUTE_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" # deterministic UUID -GREENFIELD_URN = "TEST-GFA-001" -GREENFIELD_NAME = "Greenfield Academy" - -# ─── User definitions ──────────────────────────────────────────────────────── -# Format: email, password, username, full_name, display_name, user_type, role, institute_id -TEST_USERS: List[Dict] = [ - # ── KevlarAI students ──────────────────────────────────────────────────── - { - "email": "student1@kevlarai.com", - "password": "Student1@KevlarAI!", - "username": "student1.kevlarai", - "full_name": "Alice Nguyen", - "display_name": "Alice", - "user_type": "student", - "role": "student", - "institute_id": KEVLARAI_INSTITUTE_ID, - "institute_db": KEVLARAI_INSTITUTE_DB, - "metadata": {"year_group": "Year 10"}, - }, - { - "email": "student2@kevlarai.com", - "password": "Student2@KevlarAI!", - "username": "student2.kevlarai", - "full_name": "Ben Okafor", - "display_name": "Ben", - "user_type": "student", - "role": "student", - "institute_id": KEVLARAI_INSTITUTE_ID, - "institute_db": KEVLARAI_INSTITUTE_DB, - "metadata": {"year_group": "Year 10"}, - }, - { - "email": "student3@kevlarai.com", - "password": "Student3@KevlarAI!", - "username": "student3.kevlarai", - "full_name": "Chloe Park", - "display_name": "Chloe", - "user_type": "student", - "role": "student", - "institute_id": KEVLARAI_INSTITUTE_ID, - "institute_db": KEVLARAI_INSTITUTE_DB, - "metadata": {"year_group": "Year 11"}, - }, - # ── Greenfield Academy admin ───────────────────────────────────────────── - { - "email": "head@greenfieldacademy.test", - "password": "Admin@Greenfield1!", - "username": "head.greenfield", - "full_name": "Dr James Whitmore", - "display_name": "Dr Whitmore", - "user_type": "teacher", - "role": "school_admin", - "institute_id": GREENFIELD_INSTITUTE_ID, - "institute_db": None, # populated after school provisioning - "metadata": {}, - }, - # ── Greenfield teachers ────────────────────────────────────────────────── - { - "email": "physics@greenfieldacademy.test", - "password": "Teacher1@Greenfield1!", - "username": "physics.greenfield", - "full_name": "Priya Sharma", - "display_name": "Priya", - "user_type": "teacher", - "role": "teacher", - "institute_id": GREENFIELD_INSTITUTE_ID, - "institute_db": None, - "metadata": {"subject": "Physics"}, - }, - { - "email": "english@greenfieldacademy.test", - "password": "Teacher2@Greenfield1!", - "username": "english.greenfield", - "full_name": "Tom Bradley", - "display_name": "Tom", - "user_type": "teacher", - "role": "teacher", - "institute_id": GREENFIELD_INSTITUTE_ID, - "institute_db": None, - "metadata": {"subject": "English"}, - }, - # ── Greenfield students ────────────────────────────────────────────────── - { - "email": "alice@greenfieldacademy.test", - "password": "Student1@Greenfield1!", - "username": "alice.greenfield", - "full_name": "Alice Thornton", - "display_name": "Alice T", - "user_type": "student", - "role": "student", - "institute_id": GREENFIELD_INSTITUTE_ID, - "institute_db": None, - "metadata": {"year_group": "Year 9"}, - }, - { - "email": "bob@greenfieldacademy.test", - "password": "Student2@Greenfield1!", - "username": "bob.greenfield", - "full_name": "Bob Ivanov", - "display_name": "Bob", - "user_type": "student", - "role": "student", - "institute_id": GREENFIELD_INSTITUTE_ID, - "institute_db": None, - "metadata": {"year_group": "Year 9"}, - }, - { - "email": "carol@greenfieldacademy.test", - "password": "Student3@Greenfield1!", - "username": "carol.greenfield", - "full_name": "Carol Mensah", - "display_name": "Carol", - "user_type": "student", - "role": "student", - "institute_id": GREENFIELD_INSTITUTE_ID, - "institute_db": None, - "metadata": {"year_group": "Year 10"}, - }, -] +from run.initialization.seed_environment import seed def seed_test_environment() -> Dict[str, Any]: - from modules.database.supabase.utils.client import SupabaseServiceRoleClient - from modules.database.services.provisioning_service import ProvisioningService + """Seed the lightweight 9-user test environment.""" + return seed(test=True) - sb_client = SupabaseServiceRoleClient() - supabase_url = os.environ["SUPABASE_URL"] - service_key = os.environ["SERVICE_ROLE_KEY"] - headers = { - "apikey": service_key, - "Authorization": f"Bearer {service_key}", - "Content-Type": "application/json", - } +if __name__ == "__main__": + import json - def auth_get(path, params=None): - r = requests.get(f"{supabase_url}/auth/v1/admin{path}", headers=headers, params=params) - r.raise_for_status() - return r.json() - - def auth_post(path, data): - r = requests.post(f"{supabase_url}/auth/v1/admin{path}", headers=headers, json=data) - return r - - def sb_upsert(table, data, on_conflict): - h = {**headers, "Prefer": "resolution=merge-duplicates,return=representation"} - r = requests.post( - f"{supabase_url}/rest/v1/{table}", - headers=h, - json=data, - params={"on_conflict": on_conflict}, - ) - return r - - def sb_select(table, eq_col, eq_val): - r = requests.get( - f"{supabase_url}/rest/v1/{table}", - headers=headers, - params={"select": "*", eq_col: f"eq.{eq_val}"}, - ) - r.raise_for_status() - return r.json() - - errors: List[str] = [] - results: Dict[str, Any] = {"steps": {}} - - # ── Step 1: Ensure kcar is a platform super-admin ───────────────────────── - logger.info("Step 1: Platform super-admin setup...") - try: - kcar_id = "d9e1d1a9-04c4-4611-bb05-57babf4a9a28" # known from profiles - r = sb_upsert("admin_profiles", { - "id": kcar_id, - "email": "kcar@kevlarai.com", - "display_name": "Kevin Carroll", - "admin_role": "super_admin", - "is_super_admin": True, - "metadata": {"seeded": True}, - }, on_conflict="id") - if r.status_code in (200, 201): - logger.info(" kcar → admin_profiles super_admin ✓") - results["steps"]["super_admin"] = "ok" - else: - raise Exception(f"Upsert failed: {r.text[:200]}") - except Exception as e: - msg = f"super_admin setup: {e}" - logger.error(f" {msg}") - errors.append(msg) - results["steps"]["super_admin"] = "error" - - # ── Step 2: Provision Greenfield Academy ────────────────────────────────── - logger.info("Step 2: Greenfield Academy school provisioning...") - greenfield_db = None - try: - # Check if already exists - existing = sb_select("institutes", "id", GREENFIELD_INSTITUTE_ID) - if not existing: - # Determine neo4j_uuid_string (same sanitization as provisioning_service) - neo4j_uuid = GREENFIELD_INSTITUTE_ID.replace("-", "") - r = sb_upsert("institutes", { - "id": GREENFIELD_INSTITUTE_ID, - "name": GREENFIELD_NAME, - "urn": GREENFIELD_URN, - "status": "active", - "address": {"line1": "1 Academy Road", "city": "Testville", "postcode": "TE1 1ST"}, - "website": "https://greenfieldacademy.test", - "metadata": {"headteacher": "Dr James Whitmore", "seeded": True}, - "neo4j_uuid_string": neo4j_uuid, - }, on_conflict="id") - if r.status_code not in (200, 201): - raise Exception(f"Institute upsert: {r.text[:200]}") - logger.info(f" Greenfield Academy created [{GREENFIELD_INSTITUTE_ID[:8]}]") - - # Provision Neo4j DB - provisioner = ProvisioningService() - prov_result = provisioner.ensure_school(GREENFIELD_INSTITUTE_ID) - greenfield_db = prov_result.get("db_name") - logger.info(f" Neo4j DB provisioned: {greenfield_db}") - else: - neo4j_uuid = existing[0].get("neo4j_uuid_string") or GREENFIELD_INSTITUTE_ID.replace("-", "") - greenfield_db = f"cc.institutes.{neo4j_uuid}" - logger.info(f" Greenfield Academy already exists → {greenfield_db}") - - results["steps"]["greenfield_school"] = greenfield_db - except Exception as e: - msg = f"greenfield_school: {e}" - logger.error(f" {msg}") - errors.append(msg) - results["steps"]["greenfield_school"] = "error" - greenfield_db = f"cc.institutes.{GREENFIELD_INSTITUTE_ID.replace('-', '')}" - - # Update institute_db for Greenfield users - for u in TEST_USERS: - if u["institute_id"] == GREENFIELD_INSTITUTE_ID: - u["institute_db"] = greenfield_db - - # ── Step 3: Create / verify all test users ──────────────────────────────── - logger.info("Step 3: Creating test users...") - created_users: Dict[str, Dict] = {} - try: - all_users = auth_get("/users", params={"per_page": 200}).get("users", []) - existing_by_email = {u["email"]: u for u in all_users} - except Exception as e: - msg = f"auth/users list: {e}" - logger.error(msg) - errors.append(msg) - existing_by_email = {} - - for spec in TEST_USERS: - email = spec["email"] - if email in existing_by_email: - uid = existing_by_email[email]["id"] - logger.info(f" {email}: exists [{uid[:8]}]") - created_users[email] = {"id": uid, **spec} - continue - - r = auth_post("/users", { - "email": email, - "password": spec["password"], - "email_confirm": True, - "user_metadata": { - "username": spec["username"], - "full_name": spec["full_name"], - "display_name": spec["display_name"], - "user_type": spec["user_type"], - }, - }) - if r.status_code in (200, 201): - uid = r.json()["id"] - logger.info(f" {email}: created [{uid[:8]}]") - created_users[email] = {"id": uid, **spec} - else: - msg = f"create {email}: {r.text[:200]}" - logger.error(f" {msg}") - errors.append(msg) - time.sleep(0.25) - - results["steps"]["users_created"] = list(created_users.keys()) - - # ── Step 4: Upsert profiles + memberships ───────────────────────────────── - logger.info("Step 4: Upserting profiles and memberships...") - for spec in TEST_USERS: - u = created_users.get(spec["email"]) - if not u: - continue - try: - sb_upsert("profiles", { - "id": u["id"], - "email": spec["email"], - "user_type": spec["user_type"], - "username": spec["username"], - "full_name": spec["full_name"], - "display_name": spec["display_name"], - "school_id": spec["institute_id"], - "neo4j_sync_status": "pending", - }, on_conflict="id") - - sb_upsert("institute_memberships", { - "profile_id": u["id"], - "institute_id": spec["institute_id"], - "role": spec["role"], - "metadata": spec.get("metadata", {}), - }, on_conflict="profile_id,institute_id") - except Exception as e: - msg = f"profile/membership {spec['email']}: {e}" - logger.error(f" {msg}") - errors.append(msg) - - results["steps"]["profiles_memberships"] = "ok" - - # ── Step 5: Neo4j Teacher/Student nodes for all users ──────────────────── - logger.info("Step 5: Creating Neo4j worker nodes...") - try: - from neo4j import GraphDatabase - driver = GraphDatabase.driver("bolt://192.168.0.209:7687", auth=("neo4j", "&%N304j&%")) - - # Group users by institute DB - by_db: Dict[str, List[Dict]] = {} - for spec in TEST_USERS: - u = created_users.get(spec["email"]) - if not u or not spec.get("institute_db"): - continue - by_db.setdefault(spec["institute_db"], []).append({**spec, "uid": u["id"]}) - - for db, users in by_db.items(): - with driver.session(database=db) as s: - for u in users: - label = "Teacher" if u["user_type"] == "teacher" else "Student" - s.run( - f"MERGE (n:{label} {{uuid_string: $uid}}) " - "SET n.worker_email = $email, " - " n.worker_name = $name, " - " n.unique_id = $uid, " - " n.user_type = $user_type, " - " n.worker_type = $user_type", - uid=u["uid"], email=u["email"], - name=u["full_name"], user_type=u["user_type"], - ) - logger.info(f" [{db[:30]}] {label}: {u['email']}") - - driver.close() - results["steps"]["neo4j_nodes"] = "ok" - except Exception as e: - msg = f"neo4j_nodes: {e}" - logger.error(f" {msg}") - errors.append(msg) - results["steps"]["neo4j_nodes"] = "error" - - # ── Summary ─────────────────────────────────────────────────────────────── - results["success"] = len(errors) == 0 - results["errors"] = errors - results["message"] = ( - f"Seed complete — {len(created_users)} users across 2 schools" - if not errors - else f"{len(errors)} error(s): {errors[0]}" - ) - - # Print credential sheet - logger.info("\n" + "=" * 60) - logger.info("TEST CREDENTIAL SHEET") - logger.info("=" * 60) - logger.info(f"{'ROLE':<20} {'EMAIL':<40} {'PASSWORD'}") - logger.info("-" * 90) - logger.info(f"{'[PLATFORM ADMIN]':<20} {'kcar@kevlarai.com':<40} KevlarAI2025!") - logger.info("-" * 90) - logger.info(f"[KevlarAI School]") - for spec in TEST_USERS: - if spec["institute_id"] == KEVLARAI_INSTITUTE_ID: - logger.info(f" {spec['role']:<18} {spec['email']:<40} {spec['password']}") - logger.info("-" * 90) - logger.info(f"[Greenfield Academy]") - for spec in TEST_USERS: - if spec["institute_id"] == GREENFIELD_INSTITUTE_ID: - logger.info(f" {spec['role']:<18} {spec['email']:<40} {spec['password']}") - logger.info("=" * 60) - - return results + print(json.dumps(seed_test_environment(), indent=2, default=str)) diff --git a/start.sh b/start.sh index c7965a4..546c75b 100755 --- a/start.sh +++ b/start.sh @@ -2,7 +2,7 @@ # ClassroomCopilot Startup Script # Usage: ./start.sh [start_mode] -# start_mode options: infra, demo-school, demo-users, gais-data, full, dev, prod +# start_mode options: infra, seed, seed-test, gais-data, full, dev, prod set -e @@ -14,10 +14,10 @@ show_help() { echo "" echo "Start modes:" echo " infra - Setup infrastructure (Neo4j schema, calendar, Supabase buckets)" - echo " demo-school - Create demo school (KevlarAI)" - echo " demo-users - Create demo users" + echo " seed - Seed canonical full environment (20 school users)" + echo " seed-test - Seed lightweight test environment (9 school users)" echo " gais-data - Import GAIS data (Edubase, etc.)" - echo " full - Run full initialization (infra → demo-school → demo-users → gais-data)" + echo " full - Run full initialization (infra → seed)" echo " nuke - 💥 NUKE Redis - Clear all queue data for fresh start" echo " dev - Run development server with auto-reload" echo " prod - Run production server (for Docker/containerized deployment)" @@ -25,8 +25,8 @@ show_help() { echo "Examples:" echo " ./start.sh # Run in dev mode (default)" echo " ./start.sh infra # Setup infrastructure" - echo " ./start.sh demo-school # Create demo school" - echo " ./start.sh demo-users # Create demo users" + echo " ./start.sh seed # Seed canonical full environment" + echo " ./start.sh seed-test # Seed lightweight test environment" echo " ./start.sh gais-data # Import GAIS data" echo " ./start.sh full # Run full initialization" echo " ./start.sh full --yes # Run full initialization without prompts" @@ -133,54 +133,32 @@ run_infra() { fi } -# Function to run demo school creation -run_demo_school() { - print_status "Running demo school creation mode..." - print_status "This will create the KevlarAI demo school." +# Function to run canonical environment seed +run_seed() { + local mode=${1:-seed} + if [[ "$mode" == "seed-test" ]]; then + print_status "Running lightweight seed-test mode (9 school users)..." + else + print_status "Running canonical seed mode (20 school users)..." + fi # Check if we should proceed if [[ "$AUTO_YES" != true ]]; then - read -p "Do you want to continue with demo school creation? (y/N): " -n 1 -r + read -p "Do you want to continue with $mode? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then - print_status "Demo school creation cancelled." + print_status "$mode cancelled." exit 0 fi fi - print_status "Starting demo school creation process..." - $PYTHON_CMD main.py --mode demo-school + print_status "Starting $mode process..." + $PYTHON_CMD main.py --mode "$mode" if [ $? -eq 0 ]; then - print_success "Demo school creation completed successfully!" + print_success "$mode completed successfully!" else - print_error "Demo school creation failed!" - exit 1 - fi -} - -# Function to run demo users creation -run_demo_users() { - print_status "Running demo users creation mode..." - print_status "This will create demo users for testing." - - # Check if we should proceed - if [[ "$AUTO_YES" != true ]]; then - read -p "Do you want to continue with demo users creation? (y/N): " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - print_status "Demo users creation cancelled." - exit 0 - fi - fi - - print_status "Starting demo users creation process..." - $PYTHON_CMD main.py --mode demo-users - - if [ $? -eq 0 ]; then - print_success "Demo users creation completed successfully!" - else - print_error "Demo users creation failed!" + print_error "$mode failed!" exit 1 fi } @@ -274,7 +252,7 @@ except Exception as e: # Function to run full initialization (all steps in order) run_full() { - print_status "Running full initialization (infra → demo-school → demo-users → gais-data)..." + print_status "Running full initialization (infra → seed)..." # Single confirmation for the whole flow if [[ "$AUTO_YES" != true ]]; then @@ -289,14 +267,8 @@ run_full() { # Run infra run_infra || { print_error "Full init aborted during infra."; exit 1; } - # Run demo school - run_demo_school || { print_error "Full init aborted during demo-school."; exit 1; } - - # Run demo users - run_demo_users || { print_error "Full init aborted during demo-users."; exit 1; } - - # Run GAIS data import - run_gais_data || { print_error "Full init aborted during gais-data."; exit 1; } + # Run canonical full seed + run_seed seed || { print_error "Full init aborted during seed."; exit 1; } print_success "Full initialization completed successfully!" } @@ -383,11 +355,11 @@ main() { "infra") run_infra ;; - "demo-school") - run_demo_school + "seed") + run_seed seed ;; - "demo-users") - run_demo_users + "seed-test") + run_seed seed-test ;; "gais-data") run_gais_data @@ -406,7 +378,7 @@ main() { ;; *) print_error "Invalid start mode: $START_MODE" - print_status "Valid modes: infra, demo-school, demo-users, gais-data, nuke, dev, prod" + print_status "Valid modes: infra, seed, seed-test, gais-data, full, nuke, dev, prod" print_status "Usage: ./start.sh [start_mode]" print_status "Use './start.sh --help' for more information" exit 1