diff --git a/backend/app/algorithms/models.py b/backend/app/algorithms/models.py index e3262cf..52f559d 100644 --- a/backend/app/algorithms/models.py +++ b/backend/app/algorithms/models.py @@ -33,6 +33,7 @@ class TimeBlockAssignmentResult: assignments: list[SectionAssignment] warnings: list[Warning] + warning_section_ids: list[int | None] = field(default_factory=list) @dataclass diff --git a/backend/app/algorithms/time_assignment.py b/backend/app/algorithms/time_assignment.py index 9b4b0c0..1cb2d80 100644 --- a/backend/app/algorithms/time_assignment.py +++ b/backend/app/algorithms/time_assignment.py @@ -96,6 +96,7 @@ def assign_time_blocks( cap = float(params.MaxTimeBlockCapacity) # 0.15 warnings: list[Warning] = [] + warning_section_ids: list[int | None] = [] out: list[SectionAssignment] = [] # set of time_block_ids already assigned to them. @@ -194,6 +195,7 @@ def prefs_for(nuid: int) -> Sequence[MeetingPreferenceInfo]: BlockID=None, ) ) + warning_section_ids.append(assign.section_id) # return all the SectionAssignments we have so far # plus the ones with time_block_id=None to indicate unplaced out.append( @@ -228,7 +230,7 @@ def prefs_for(nuid: int) -> Sequence[MeetingPreferenceInfo]: # Final sort by section_id for display out.sort(key=lambda s: s.section_id) - return TimeBlockAssignmentResult(assignments=out, warnings=warnings) + return TimeBlockAssignmentResult(assignments=out, warnings=warnings, warning_section_ids=warning_section_ids) # The following function is the exact same department cap logic used in assign_time_blocks diff --git a/backend/app/repositories/schedule_warning.py b/backend/app/repositories/schedule_warning.py index b524111..f20d75e 100644 --- a/backend/app/repositories/schedule_warning.py +++ b/backend/app/repositories/schedule_warning.py @@ -44,25 +44,35 @@ def sync_section_warnings( schedule_id: int, detected: list[WarningType], ) -> None: - + # Replace strategy with dismissed preservation: + # - drop any row whose type is no longer detected (condition resolved) + # - for types still detected: keep dismissed rows, drop non-dismissed duplicates, + # and add exactly one non-dismissed row per type (unless a dismissed row already exists). + # Diff-based reconciliation can't clean up duplicates (error_check can emit the same + # type once per faculty), so it was orphaning rows. existing = get_by_section(db, section_id) - existing_by_type = {w.type: w for w in existing} detected_values = {wt.value for wt in detected} - - for type_str, warning in existing_by_type.items(): - if type_str not in detected_values: - db.delete(warning) - - for wt in detected: - if wt.value not in existing_by_type: - db.add( - ScheduleWarning( - schedule_id=schedule_id, - section_id=section_id, - type=wt.value, - severity=str(wt.severity.value), - message=wt.value, - dismissed=False, - ) + dismissed_types: set[str] = set() + + for w in existing: + if w.type not in detected_values: + db.delete(w) + elif w.dismissed: + dismissed_types.add(w.type) + else: + db.delete(w) + + for wt in set(detected): + if wt.value in dismissed_types: + continue + db.add( + ScheduleWarning( + schedule_id=schedule_id, + section_id=section_id, + type=wt.value, + severity=str(wt.severity.value), + message=wt.value, + dismissed=False, ) + ) db.commit() diff --git a/backend/app/routers/schedule_warning.py b/backend/app/routers/schedule_warning.py index efb1939..c1bf93e 100644 --- a/backend/app/routers/schedule_warning.py +++ b/backend/app/routers/schedule_warning.py @@ -3,12 +3,15 @@ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session +from app.core.auth import require_admin from app.core.database import get_db from app.core.enums import Severity, WarningType from app.models.schedule_warning import ScheduleWarning as ScheduleWarningModel +from app.models.user import User from app.repositories import schedule as schedule_repo from app.repositories import schedule_warning as warning_repo from app.schemas.warning import Warning, WarningResponse +from app.services.connection_manager import manager router = APIRouter(prefix="/schedules", tags=["warnings"]) @@ -24,6 +27,7 @@ def _to_response(r: ScheduleWarningModel) -> WarningResponse: CourseID=r.course_id, BlockID=r.time_block_id, dismissed=r.dismissed, + dismissed_by=r.dismissed_by, ) @@ -74,10 +78,11 @@ def create_warning( @router.patch("/{schedule_id}/warnings/{warning_id}/dismiss") -def dismiss_warning( +async def dismiss_warning( schedule_id: int, warning_id: int, db: Session = Depends(get_db), + current_user: User = Depends(require_admin), ): if not schedule_repo.schedule_exists(db, schedule_id): raise HTTPException(status_code=404, detail="Schedule not found") @@ -87,15 +92,25 @@ def dismiss_warning( raise HTTPException(status_code=404, detail="Warning not found") warning.dismissed = True + warning.dismissed_by = f"{current_user.first_name} {current_user.last_name}" db.commit() + + await manager.broadcast( + schedule_id, + { + "type": "section_warnings", + "payload": {"section_id": warning.section_id, "warnings": []}, + }, + ) return {"warning_id": warning_id, "dismissed": True} @router.patch("/{schedule_id}/warnings/{warning_id}/restore") -def restore_warning( +async def restore_warning( schedule_id: int, warning_id: int, db: Session = Depends(get_db), + current_user: User = Depends(require_admin), ): if not schedule_repo.schedule_exists(db, schedule_id): raise HTTPException(status_code=404, detail="Schedule not found") @@ -105,15 +120,25 @@ def restore_warning( raise HTTPException(status_code=404, detail="Warning not found") warning.dismissed = False + warning.dismissed_by = None db.commit() + + await manager.broadcast( + schedule_id, + { + "type": "section_warnings", + "payload": {"section_id": warning.section_id, "warnings": []}, + }, + ) return {"warning_id": warning_id, "dismissed": False} @router.delete("/{schedule_id}/warnings/{warning_id}", status_code=204) -def delete_warning( +async def delete_warning( schedule_id: int, warning_id: int, db: Session = Depends(get_db), + current_user: User = Depends(require_admin), ): if not schedule_repo.schedule_exists(db, schedule_id): raise HTTPException(status_code=404, detail="Schedule not found") @@ -122,5 +147,14 @@ def delete_warning( if warning is None or warning.schedule_id != schedule_id: raise HTTPException(status_code=404, detail="Warning not found") + section_id = warning.section_id db.delete(warning) db.commit() + + await manager.broadcast( + schedule_id, + { + "type": "section_warnings", + "payload": {"section_id": section_id, "warnings": []}, + }, + ) diff --git a/backend/app/routers/section.py b/backend/app/routers/section.py index a5b9fe6..07ee577 100644 --- a/backend/app/routers/section.py +++ b/backend/app/routers/section.py @@ -101,17 +101,18 @@ async def update_section( }, ) - if warnings: - await manager.broadcast( - updated.schedule_id, - { - "type": "section_warnings", - "payload": { - "section_id": section_id, - "warnings": [w.value for w in warnings], - }, + # Broadcast unconditionally: an edit that clears the last warning still needs to + # prompt clients to refetch, otherwise stale warning state lingers in the UI. + await manager.broadcast( + updated.schedule_id, + { + "type": "section_warnings", + "payload": { + "section_id": section_id, + "warnings": [w.value for w in warnings], }, - ) + }, + ) return updated diff --git a/backend/app/schemas/warning.py b/backend/app/schemas/warning.py index 5587fa9..56117f7 100644 --- a/backend/app/schemas/warning.py +++ b/backend/app/schemas/warning.py @@ -19,4 +19,5 @@ class WarningResponse(Warning): warning_id: int = Field(..., description="Unique warning ID") dismissed: bool = Field(default=False, description="Whether this warning was dismissed") + dismissed_by: str | None = Field(default=None, description="Display name of the user who dismissed the warning") section_id: int | None = Field(default=None, description="Directly linked section (manual-edit warnings only)") diff --git a/backend/app/services/algorithm.py b/backend/app/services/algorithm.py index eea1116..b1a6e90 100644 --- a/backend/app/services/algorithm.py +++ b/backend/app/services/algorithm.py @@ -91,13 +91,25 @@ def _set_status(db, schedule_id: int, status: str, error_message: str | None = N # Warning persistence — respects dismissed warnings -def _persist_warnings(db, schedule_id, phase2_warnings, unmatched_assignments): - """Persist warnings, preserving dismissed ones and skipping duplicates.""" +def _persist_warnings( + db, + schedule_id, + phase2_warnings, + phase2_warning_section_ids, + unmatched_assignments, + algo_to_db_section_id, +): + """Persist warnings, preserving dismissed ones and skipping duplicates. + + ``algo_to_db_section_id`` maps each algorithm-internal section id to the real DB + section id that was just written, so row-level warnings land on the right row. + """ # Load all existing warnings (including dismissed) existing = warning_repo.get_by_schedule(db, schedule_id, include_dismissed=True) - # Build set of dismissed warning keys so we don't re-create them - dismissed_keys = {(w.type, w.course_id, w.faculty_nuid, w.time_block_id) for w in existing if w.dismissed} + # Build set of dismissed warning keys so we don't re-create them. + # section_id is part of the key so per-section dismissals survive across runs. + dismissed_keys = {(w.type, w.section_id, w.course_id, w.faculty_nuid, w.time_block_id) for w in existing if w.dismissed} # Delete only non-dismissed warnings (dismissed ones survive re-runs) for w in existing: @@ -107,19 +119,17 @@ def _persist_warnings(db, schedule_id, phase2_warnings, unmatched_assignments): count = 0 # Persist Phase 2 warnings (time block assignment issues) - for w in phase2_warnings: - key = ( - w.Type.value if w.Type else "unknown", - w.CourseID, - w.FacultyID, - w.BlockID, - ) + for w, algo_sid in zip(phase2_warnings, phase2_warning_section_ids, strict=True): + real_sid = algo_to_db_section_id.get(algo_sid) if algo_sid is not None else None + type_value = w.Type.value if w.Type else "unknown" + key = (type_value, real_sid, w.CourseID, w.FacultyID, w.BlockID) if key in dismissed_keys: continue db.add( ScheduleWarningModel( schedule_id=schedule_id, - type=key[0], + section_id=real_sid, + type=type_value, severity=w.SeverityRank.value, message=w.Message, faculty_nuid=w.FacultyID, @@ -131,12 +141,14 @@ def _persist_warnings(db, schedule_id, phase2_warnings, unmatched_assignments): # Persist unmatched section warnings for a in unmatched_assignments: - key = (WarningType.INSUFFICIENT_FACULTY_SUPPLY.value, a.course_id, None, None) + real_sid = algo_to_db_section_id.get(a.section_id) + key = (WarningType.INSUFFICIENT_FACULTY_SUPPLY.value, real_sid, a.course_id, None, None) if key in dismissed_keys: continue db.add( ScheduleWarningModel( schedule_id=schedule_id, + section_id=real_sid, type=WarningType.INSUFFICIENT_FACULTY_SUPPLY.value, severity=str(WarningType.INSUFFICIENT_FACULTY_SUPPLY.severity.value), message=f"Section {a.section_id} unmatched: {a.unmatched_reason}", @@ -148,6 +160,37 @@ def _persist_warnings(db, schedule_id, phase2_warnings, unmatched_assignments): logger.info(f"Persisted {count} warnings ({len(dismissed_keys)} dismissed preserved)") +def _persist_per_section_warnings(db, schedule_id: int, section_ids: list[int]) -> int: + """Run error_check on each section and stage ScheduleWarning rows (no commit).""" + from app.services import section as section_service + + # Refresh schedule so its sections relationship reflects the just-written rows. + schedule = db.query(Schedule).filter(Schedule.schedule_id == schedule_id).first() + if schedule is not None: + db.refresh(schedule) + + count = 0 + for sid in section_ids: + section = section_repo.get_by_id(db, sid) + if section is None: + continue + # error_check appends once per faculty, so the same type can repeat. + # Dedupe so each section gets at most one row per type. + for wt in set(section_service.error_check(db, section)): + db.add( + ScheduleWarningModel( + schedule_id=schedule_id, + section_id=sid, + type=wt.value, + severity=str(wt.severity.value), + message=wt.value, + ) + ) + count += 1 + logger.info(f"Persisted {count} per-section warnings") + return count + + # Generate — full run from scratch @@ -246,6 +289,7 @@ def _run_algorithm(db, schedule_id: int, parameters: AlgorithmParameters): matched_lookup = {a.section_id: a for a in matched} section_number_tracker: dict[int, int] = {} sections_written = 0 + algo_to_db_section_id: dict[int, int] = {} # Matched sections (placed or unplaced) — write with faculty, time block optional for sa in phase2_result.assignments: @@ -263,6 +307,7 @@ def _run_algorithm(db, schedule_id: int, parameters: AlgorithmParameters): ) db.add(section_obj) db.flush() + algo_to_db_section_id[sa.section_id] = section_obj.section_id fa = FacultyAssignment( faculty_nuid=original.faculty_nuid, section_id=section_obj.section_id, @@ -281,10 +326,20 @@ def _run_algorithm(db, schedule_id: int, parameters: AlgorithmParameters): capacity=30, ) db.add(section_obj) + db.flush() + algo_to_db_section_id[a.section_id] = section_obj.section_id sections_written += 1 # Step 9: Persist warnings (respects dismissed) - _persist_warnings(db, schedule_id, phase2_result.warnings, unmatched) + _persist_warnings( + db, + schedule_id, + phase2_result.warnings, + phase2_result.warning_section_ids, + unmatched, + algo_to_db_section_id, + ) + _persist_per_section_warnings(db, schedule_id, list(algo_to_db_section_id.values())) db.commit() logger.info(f"Algorithm completed for schedule {schedule_id}: {sections_written} sections written") @@ -427,6 +482,7 @@ def _run_regenerate(db, schedule_id: int, parameters: AlgorithmParameters): section_number_tracker[s.course_id] = max(current, s.section_number) sections_written = 0 + algo_to_db_section_id: dict[int, int] = {} # Matched sections (placed or unplaced) — write with faculty, time block optional for sa in phase2_result.assignments: @@ -444,6 +500,7 @@ def _run_regenerate(db, schedule_id: int, parameters: AlgorithmParameters): ) db.add(section_obj) db.flush() + algo_to_db_section_id[sa.section_id] = section_obj.section_id fa = FacultyAssignment( faculty_nuid=original.faculty_nuid, section_id=section_obj.section_id, @@ -462,10 +519,20 @@ def _run_regenerate(db, schedule_id: int, parameters: AlgorithmParameters): capacity=30, ) db.add(section_obj) + db.flush() + algo_to_db_section_id[a.section_id] = section_obj.section_id sections_written += 1 # Step 11: Persist warnings (append to existing, respect dismissed) - _persist_warnings(db, schedule_id, phase2_result.warnings, unmatched) + _persist_warnings( + db, + schedule_id, + phase2_result.warnings, + phase2_result.warning_section_ids, + unmatched, + algo_to_db_section_id, + ) + _persist_per_section_warnings(db, schedule_id, list(algo_to_db_section_id.values())) db.commit() logger.info(f"Regenerate completed for schedule {schedule_id}: {sections_written} new sections written") diff --git a/backend/tests/test_algorithm_service.py b/backend/tests/test_algorithm_service.py index 0d7819b..6e29b3b 100644 --- a/backend/tests/test_algorithm_service.py +++ b/backend/tests/test_algorithm_service.py @@ -33,7 +33,7 @@ def test_persist_warnings_creates_unmatched_rows(db_session): db_session.commit() unmatched = [CourseAssignment(section_id=1, course_id=42, is_matched=False, unmatched_reason="No qualified faculty")] - _persist_warnings(db_session, schedule.schedule_id, [], unmatched) + _persist_warnings(db_session, schedule.schedule_id, [], [], unmatched, {}) db_session.flush() rows = db_session.query(ScheduleWarning).filter(ScheduleWarning.schedule_id == schedule.schedule_id).all() @@ -67,7 +67,7 @@ def test_persist_warnings_skips_dismissed_unmatched(db_session): db_session.commit() unmatched = [CourseAssignment(section_id=1, course_id=42, is_matched=False, unmatched_reason="No qualified faculty")] - _persist_warnings(db_session, schedule.schedule_id, [], unmatched) + _persist_warnings(db_session, schedule.schedule_id, [], [], unmatched, {}) db_session.flush() rows = db_session.query(ScheduleWarning).filter(ScheduleWarning.schedule_id == schedule.schedule_id).all() @@ -97,7 +97,7 @@ def test_persist_warnings_replaces_non_dismissed_on_rerun(db_session): db_session.add(old) db_session.commit() - _persist_warnings(db_session, schedule.schedule_id, [], []) + _persist_warnings(db_session, schedule.schedule_id, [], [], [], {}) db_session.flush() rows = db_session.query(ScheduleWarning).filter(ScheduleWarning.schedule_id == schedule.schedule_id).all() diff --git a/frontend/openapi.json b/frontend/openapi.json index 3c23173..4581ac6 100644 --- a/frontend/openapi.json +++ b/frontend/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Automated Course Scheduler API","description":"API for the Automated Course Scheduler system","version":"1.0.0"},"paths":{"/sections":{"post":{"tags":["sections"],"summary":"Create Section","description":"Create a new section in a schedule.","operationId":"create_section_sections_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectionCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sections/{section_id}":{"patch":{"tags":["sections"],"summary":"Update Section","description":"Acquire (or refresh) the lock on this section, then apply the update.\n\nFails with 423 if another user currently holds the lock.\nBroadcasts section_updated to connected clients on success.","operationId":"update_section_sections__section_id__patch","parameters":[{"name":"section_id","in":"path","required":true,"schema":{"type":"integer","title":"Section Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectionUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sections"],"summary":"Delete Section","description":"Delete a section from a schedule.","operationId":"delete_section_sections__section_id__delete","parameters":[{"name":"section_id","in":"path","required":true,"schema":{"type":"integer","title":"Section Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules":{"get":{"tags":["schedules"],"summary":"Get Schedules","operationId":"get_schedules_schedules_get","parameters":[{"name":"campus_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Campus Id"}},{"name":"semester_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Semester Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleResponse"},"title":"Response Get Schedules Schedules Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["schedules"],"summary":"Create Schedule","description":"Create a new schedule draft.","operationId":"create_schedule_schedules_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}":{"get":{"tags":["schedules"],"summary":"Get Schedule","description":"Retrieve a specific schedule.","operationId":"get_schedule_schedules__schedule_id__get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["schedules"],"summary":"Update Schedule","description":"Update schedule metadata (name, complete status, etc.).","operationId":"update_schedule_schedules__schedule_id__put","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["schedules"],"summary":"Delete Schedule","description":"Delete a schedule and all its sections.","operationId":"delete_schedule_schedules__schedule_id__delete","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/sections":{"get":{"tags":["schedules"],"summary":"Get Schedule Sections","description":"Get all sections for a specific schedule.","operationId":"get_schedule_sections_schedules__schedule_id__sections_get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SectionResponse"},"title":"Response Get Schedule Sections Schedules Schedule Id Sections Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/sections/rich":{"get":{"tags":["schedules"],"summary":"Get Schedule Sections Rich","description":"Get all sections with denormalized course, time block, and instructor data.","operationId":"get_schedule_sections_rich_schedules__schedule_id__sections_rich_get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SectionRichResponse"},"title":"Response Get Schedule Sections Rich Schedules Schedule Id Sections Rich Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/export/csv":{"get":{"tags":["schedules"],"summary":"Export Schedule Csv","description":"Export a finalized schedule as a downloadable CSV.","operationId":"export_schedule_csv_schedules__schedule_id__export_csv_get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/locks":{"get":{"tags":["schedules"],"summary":"Get Schedule Locks","description":"Get all active locks for a schedule.\n\nArgs:\n schedule_id: ID of the schedule to query locks for.\n db: Database session.\n\nReturns:\n List of active locks with user display name included.","operationId":"get_schedule_locks_schedules__schedule_id__locks_get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleActiveLockResponse"},"title":"Response Get Schedule Locks Schedules Schedule Id Locks Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/semesters":{"get":{"tags":["semesters"],"summary":"Get All Semesters","description":"Get all semesters, with optional filtering by campus, semester name, or year.","operationId":"get_all_semesters_semesters_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/SemesterResponse"},"type":"array","title":"Response Get All Semesters Semesters Get"}}}}}},"post":{"tags":["semesters"],"summary":"Create Semester","description":"Create a new semester.","operationId":"create_semester_semesters_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemesterCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemesterCreate"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/semesters/{semester_id}":{"get":{"tags":["semesters"],"summary":"Get Semester","description":"Retrieve a specific semester.","operationId":"get_semester_semesters__semester_id__get","parameters":[{"name":"semester_id","in":"path","required":true,"schema":{"type":"integer","title":"Semester Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemesterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["semesters"],"summary":"Update Semester","description":"Update semester metadata (name, complete status, etc.).","operationId":"update_semester_semesters__semester_id__put","parameters":[{"name":"semester_id","in":"path","required":true,"schema":{"type":"integer","title":"Semester Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemesterUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemesterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["semesters"],"summary":"Delete Semester","description":"Delete a semester and all its sections.","operationId":"delete_semester_semesters__semester_id__delete","parameters":[{"name":"semester_id","in":"path","required":true,"schema":{"type":"integer","title":"Semester Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/courses":{"get":{"tags":["courses"],"summary":"Get Courses","description":"Retrieve all courses, optionally filtered by schedule","operationId":"get_courses_courses_get","parameters":[{"name":"schedule_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filter to courses with sections in this schedule","title":"Schedule Id"},"description":"Filter to courses with sections in this schedule"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CourseResponse"},"title":"Response Get Courses Courses Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["courses"],"summary":"Create Course","description":"Create a new course.","operationId":"create_course_courses_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/courses/{course_id}":{"get":{"tags":["courses"],"summary":"Get Course","description":"Retrieve a course by ID with section count.","operationId":"get_course_courses__course_id__get","parameters":[{"name":"course_id","in":"path","required":true,"schema":{"type":"integer","title":"Course Id"}},{"name":"schedule_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Section count for this schedule only","title":"Schedule Id"},"description":"Section count for this schedule only"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["courses"],"summary":"Update Course","description":"Partially update a course.","operationId":"update_course_courses__course_id__patch","parameters":[{"name":"course_id","in":"path","required":true,"schema":{"type":"integer","title":"Course Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["courses"],"summary":"Delete Course","description":"Delete a course with no sections.","operationId":"delete_course_courses__course_id__delete","parameters":[{"name":"course_id","in":"path","required":true,"schema":{"type":"integer","title":"Course Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/faculty":{"get":{"tags":["faculty"],"summary":"Get Faculty","description":"Retrieve all faculty members.","operationId":"get_faculty_faculty_get","parameters":[{"name":"campus","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by campus name","title":"Campus"},"description":"Filter by campus name"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Filter to only active faculty","default":false,"title":"Active Only"},"description":"Filter to only active faculty"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FacultyResponse"},"title":"Response Get Faculty Faculty Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["faculty"],"summary":"Create Faculty","description":"Create a new faculty member.","operationId":"create_faculty_faculty_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacultyCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacultyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/faculty/{nuid}":{"get":{"tags":["faculty"],"summary":"Get Faculty Profile","description":"Retrieve faculty profile with course and time preferences.","operationId":"get_faculty_profile_faculty__nuid__get","parameters":[{"name":"nuid","in":"path","required":true,"schema":{"type":"integer","title":"Nuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacultyProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["faculty"],"summary":"Update Faculty","description":"Partially update faculty demographics and status.","operationId":"update_faculty_faculty__nuid__patch","parameters":[{"name":"nuid","in":"path","required":true,"schema":{"type":"integer","title":"Nuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacultyUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacultyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["faculty"],"summary":"Delete Faculty","description":"Delete a faculty member and their preferences and assignments.","operationId":"delete_faculty_faculty__nuid__delete","parameters":[{"name":"nuid","in":"path","required":true,"schema":{"type":"integer","title":"Nuid"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/faculty/build_profiles":{"post":{"tags":["faculty"],"summary":"Build Profiles","operationId":"build_profiles_faculty_build_profiles_post","requestBody":{"content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Available Faculty"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/FacultyProfileResponse"},"type":"array","title":"Response Build Profiles Faculty Build Profiles Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/time-blocks":{"get":{"tags":["time-blocks"],"summary":"Get Time Blocks","description":"Retrieve all time blocks, optionally filtered by campus.","operationId":"get_time_blocks_time_blocks_get","parameters":[{"name":"campus_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filter by campus ID","title":"Campus Id"},"description":"Filter by campus ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TimeBlockResponse"},"title":"Response Get Time Blocks Time Blocks Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["time-blocks"],"summary":"Create Time Block","description":"Create a new time block.\n\n`meeting_days` should be a compact uppercase day string (e.g. \"MWF\", \"TR\").\n`start_time` and `end_time` must be in HH:MM format.\nSet `block_group` to the same 8-character hex string on two rows to mark\nthem as a split block pair — split blocks are excluded from auto-assignment.\nReturns 409 if the block_group already has a complete pair on this campus.","operationId":"create_time_block_time_blocks_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeBlockCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeBlockResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/time-blocks/{time_block_id}":{"patch":{"tags":["time-blocks"],"summary":"Update Time Block","description":"Partially update a time block. Only fields present in the request body are changed.","operationId":"update_time_block_time_blocks__time_block_id__patch","parameters":[{"name":"time_block_id","in":"path","required":true,"schema":{"type":"integer","title":"Time Block Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeBlockUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeBlockResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["time-blocks"],"summary":"Delete Time Block","description":"Delete a time block.\n\nReturns 400 if any sections are currently assigned to this block —\nthose sections must be reassigned or removed first.","operationId":"delete_time_block_time_blocks__time_block_id__delete","parameters":[{"name":"time_block_id","in":"path","required":true,"schema":{"type":"integer","title":"Time Block Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/campuses":{"get":{"tags":["campuses"],"summary":"Get All Campuses","description":"Retrieve all campuses, optionally filtered by name.","operationId":"get_all_campuses_campuses_get","parameters":[{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CampusResponse"},"title":"Response Get All Campuses Campuses Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["campuses"],"summary":"Create Campus","description":"Create a new campus.","operationId":"create_campus_campuses_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampusCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/campuses/{campus_id}":{"get":{"tags":["campuses"],"summary":"Get Campus","description":"Retrieve a specific campus by ID.","operationId":"get_campus_campuses__campus_id__get","parameters":[{"name":"campus_id","in":"path","required":true,"schema":{"type":"integer","title":"Campus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["campuses"],"summary":"Update Campus","description":"Update campus metadata (name, etc.).","operationId":"update_campus_campuses__campus_id__put","parameters":[{"name":"campus_id","in":"path","required":true,"schema":{"type":"integer","title":"Campus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampusUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["campuses"],"summary":"Delete Campus","description":"Delete a campus.","operationId":"delete_campus_campuses__campus_id__delete","parameters":[{"name":"campus_id","in":"path","required":true,"schema":{"type":"integer","title":"Campus Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/upload/courses":{"post":{"tags":["upload"],"summary":"Upload Courses","description":"Upload a CSV file containing course offering data.","operationId":"upload_courses_upload_courses_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_courses_upload_courses_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/upload/faculty-preferences":{"post":{"tags":["upload"],"summary":"Upload Faculty Preferences","description":"Upload a CSV file containing faculty preference data.","operationId":"upload_faculty_preferences_upload_faculty_preferences_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_faculty_preferences_upload_faculty_preferences_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/upload/time-preferences":{"post":{"tags":["upload"],"summary":"Upload Time Preferences","description":"Upload a CSV file containing faculty time preference data.","operationId":"upload_time_preferences_upload_time_preferences_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_time_preferences_upload_time_preferences_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/comments":{"post":{"tags":["comments"],"summary":"Post Comment","operationId":"post_comment_comments_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentSchema"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/comments/{parent_id}":{"post":{"tags":["comments"],"summary":"Post Reply","operationId":"post_reply_comments__parent_id__post","parameters":[{"name":"parent_id","in":"path","required":true,"schema":{"type":"integer","title":"Parent Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentSchema"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/comments/{section_id}":{"get":{"tags":["comments"],"summary":"Get Comments","operationId":"get_comments_comments__section_id__get","parameters":[{"name":"section_id","in":"path","required":true,"schema":{"type":"integer","title":"Section Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CommentResponse"},"title":"Response Get Comments Comments Section Id Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/comments/{comment_id}":{"delete":{"tags":["comments"],"summary":"Delete Comment","operationId":"delete_comment_comments__comment_id__delete","parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"integer","title":"Comment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["comments"],"summary":"Resolve Comment","operationId":"resolve_comment_comments__comment_id__put","parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"integer","title":"Comment Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sections/{section_id}/lock":{"post":{"tags":["section_locks"],"summary":"Acquire Lock","description":"Acquire a lock on a section for editing.\n\nRaises:\n HTTPException: 403 if the caller does not have the admin role.\n HTTPException: 423 if the section is locked by another user.","operationId":"acquire_lock_sections__section_id__lock_post","parameters":[{"name":"section_id","in":"path","required":true,"schema":{"type":"integer","title":"Section Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectionLockResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sections/{section_id}/unlock":{"post":{"tags":["section_locks"],"summary":"Release Lock","description":"Release a lock on a section.\n\nRaises:\n HTTPException: 403 if the caller does not own an active lock.","operationId":"release_lock_sections__section_id__unlock_post","parameters":[{"name":"section_id","in":"path","required":true,"schema":{"type":"integer","title":"Section Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/warnings":{"get":{"tags":["warnings"],"summary":"Get Schedule Warnings","operationId":"get_schedule_warnings_schedules__schedule_id__warnings_get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}},{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type"}},{"name":"severity","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Severity"}},{"name":"include_dismissed","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Dismissed"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WarningResponse"},"title":"Response Get Schedule Warnings Schedules Schedule Id Warnings Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["warnings"],"summary":"Create Warning","operationId":"create_warning_schedules__schedule_id__warnings_post","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Warning"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WarningResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/warnings/{warning_id}/dismiss":{"patch":{"tags":["warnings"],"summary":"Dismiss Warning","operationId":"dismiss_warning_schedules__schedule_id__warnings__warning_id__dismiss_patch","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}},{"name":"warning_id","in":"path","required":true,"schema":{"type":"integer","title":"Warning Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/warnings/{warning_id}/restore":{"patch":{"tags":["warnings"],"summary":"Restore Warning","operationId":"restore_warning_schedules__schedule_id__warnings__warning_id__restore_patch","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}},{"name":"warning_id","in":"path","required":true,"schema":{"type":"integer","title":"Warning Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/warnings/{warning_id}":{"delete":{"tags":["warnings"],"summary":"Delete Warning","operationId":"delete_warning_schedules__schedule_id__warnings__warning_id__delete","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}},{"name":"warning_id","in":"path","required":true,"schema":{"type":"integer","title":"Warning Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/generate":{"post":{"tags":["schedules"],"summary":"Run Algorithm","operationId":"run_algorithm_schedules__schedule_id__generate_post","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateScheduleRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/regenerate":{"post":{"tags":["schedules"],"summary":"Regenerate Algorithm","operationId":"regenerate_algorithm_schedules__schedule_id__regenerate_post","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegenerateScheduleRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/invites":{"post":{"tags":["users"],"summary":"Create Invite","description":"Invite a faculty member. Requires admin role.\n\nLooks up the faculty record by NUID, creates a User, registers them in\nAuth0, generates a password-change ticket, and sends an invite email.","operationId":"create_invite_api_invites_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/invites/admin":{"post":{"tags":["users"],"summary":"Create Admin Invite","description":"Create a pending admin from NUID and name/email (no faculty record). Requires admin.","operationId":"create_admin_invite_api_invites_admin_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminInviteRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/invites/export":{"get":{"tags":["users"],"summary":"Export Invites","description":"Return invite links for all active faculty without a linked account.\n\nCreates pending User records for any who were not yet invited.\nRequires admin role.","operationId":"export_invites_api_invites_export_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/InviteLinkResponse"},"type":"array","title":"Response Export Invites Api Invites Export Get"}}}}}}},"/api/users":{"get":{"tags":["users"],"summary":"List Users","description":"Return all users. Requires admin role.","operationId":"list_users_api_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/UserResponse"},"type":"array","title":"Response List Users Api Users Get"}}}}}}},"/api/users/me":{"get":{"tags":["users"],"summary":"Get Me","description":"Return the profile of the currently authenticated user.","operationId":"get_me_api_users_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}}}},"/api/users/{user_id}":{"get":{"tags":["users"],"summary":"Get User","description":"Return a single user by ID. Requires admin role.","operationId":"get_user_api_users__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AdminInviteRequest":{"properties":{"nuid":{"type":"integer","exclusiveMinimum":0.0,"title":"Nuid"},"first_name":{"type":"string","maxLength":100,"minLength":1,"title":"First Name"},"last_name":{"type":"string","maxLength":100,"minLength":1,"title":"Last Name"},"email":{"type":"string","maxLength":100,"minLength":3,"title":"Email"}},"type":"object","required":["nuid","first_name","last_name","email"],"title":"AdminInviteRequest","description":"Pending admin user + Auth0 signup URL (no faculty row; same linking as bootstrap_admin)."},"AlgorithmParameters":{"properties":{"MaxTimeBlockCapacity":{"type":"number","maximum":1.0,"exclusiveMinimum":0.0,"title":"Maxtimeblockcapacity","description":"Max department percentage per time block","default":0.15}},"type":"object","title":"AlgorithmParameters"},"Body_upload_courses_upload_courses_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_courses_upload_courses_post"},"Body_upload_faculty_preferences_upload_faculty_preferences_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_faculty_preferences_upload_faculty_preferences_post"},"Body_upload_time_preferences_upload_time_preferences_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_time_preferences_upload_time_preferences_post"},"CampusCreate":{"properties":{"name":{"type":"string","title":"Name"},"active":{"type":"boolean","title":"Active","default":true}},"type":"object","required":["name"],"title":"CampusCreate"},"CampusResponse":{"properties":{"campus_id":{"type":"integer","title":"Campus Id"},"name":{"type":"string","title":"Name"},"active":{"type":"boolean","title":"Active"}},"type":"object","required":["campus_id","name","active"],"title":"CampusResponse"},"CampusUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Active"}},"type":"object","title":"CampusUpdate"},"CommentResponse":{"properties":{"comment_id":{"type":"integer","title":"Comment Id"},"user_id":{"type":"integer","title":"User Id"},"section_id":{"type":"integer","title":"Section Id"},"parent_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Id"},"content":{"type":"string","title":"Content"},"resolved":{"type":"boolean","title":"Resolved"},"active":{"type":"boolean","title":"Active"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"user":{"$ref":"#/components/schemas/CommentUserInfo"}},"type":"object","required":["comment_id","user_id","section_id","parent_id","content","resolved","active","created_at","user"],"title":"CommentResponse"},"CommentSchema":{"properties":{"section_id":{"type":"integer","title":"Section Id"},"user_id":{"type":"integer","title":"User Id"},"content":{"type":"string","title":"Content"}},"type":"object","required":["section_id","user_id","content"],"title":"CommentSchema"},"CommentUserInfo":{"properties":{"user_id":{"type":"integer","title":"User Id"},"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"email":{"type":"string","title":"Email"}},"type":"object","required":["user_id","first_name","last_name","email"],"title":"CommentUserInfo"},"CourseCreate":{"properties":{"subject":{"type":"string","maxLength":10,"minLength":1,"title":"Subject"},"code":{"type":"integer","exclusiveMinimum":0.0,"title":"Code"},"name":{"type":"string","minLength":1,"title":"Name"},"description":{"type":"string","minLength":1,"title":"Description"},"credits":{"type":"integer","minimum":0.0,"title":"Credits"},"priority":{"type":"boolean","title":"Priority","default":false}},"type":"object","required":["subject","code","name","description","credits"],"title":"CourseCreate"},"CourseInfo":{"properties":{"course_id":{"type":"integer","title":"Course Id"},"subject":{"type":"string","title":"Subject"},"code":{"type":"integer","title":"Code"},"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"credits":{"type":"integer","title":"Credits"}},"type":"object","required":["course_id","subject","code","name","description","credits"],"title":"CourseInfo"},"CoursePreferenceInfo":{"properties":{"course_id":{"type":"integer","title":"Course Id"},"course_name":{"type":"string","title":"Course Name"},"preference":{"type":"string","title":"Preference"}},"type":"object","required":["course_id","course_name","preference"],"title":"CoursePreferenceInfo"},"CourseResponse":{"properties":{"course_id":{"type":"integer","title":"Course Id"},"subject":{"type":"string","title":"Subject"},"code":{"type":"integer","title":"Code"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"credits":{"type":"integer","title":"Credits"},"priority":{"type":"boolean","title":"Priority","default":false},"section_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Section Count"},"qualified_faculty":{"type":"integer","title":"Qualified Faculty","default":0}},"type":"object","required":["course_id","subject","code","name","credits"],"title":"CourseResponse"},"CourseUpdate":{"properties":{"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Code"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"credits":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Credits"},"priority":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Priority"}},"type":"object","title":"CourseUpdate"},"FacultyCreate":{"properties":{"nuid":{"type":"integer","exclusiveMinimum":0.0,"title":"Nuid"},"first_name":{"type":"string","minLength":1,"title":"First Name"},"last_name":{"type":"string","minLength":1,"title":"Last Name"},"email":{"type":"string","minLength":1,"title":"Email"},"campus":{"type":"integer","exclusiveMinimum":0.0,"title":"Campus"},"active":{"type":"boolean","title":"Active","default":true},"max_load":{"type":"integer","minimum":1.0,"title":"Max Load","default":3}},"type":"object","required":["nuid","first_name","last_name","email","campus"],"title":"FacultyCreate"},"FacultyProfileResponse":{"properties":{"nuid":{"type":"integer","title":"Nuid"},"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"email":{"type":"string","title":"Email"},"campus":{"type":"integer","title":"Campus"},"active":{"type":"boolean","title":"Active"},"maxLoad":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Maxload"},"needsAdminReview":{"type":"boolean","title":"Needsadminreview","default":false},"course_preferences":{"items":{"$ref":"#/components/schemas/CoursePreferenceInfo"},"type":"array","title":"Course Preferences"},"meeting_preferences":{"items":{"$ref":"#/components/schemas/MeetingPreferenceInfo"},"type":"array","title":"Meeting Preferences"}},"type":"object","required":["nuid","first_name","last_name","email","campus","active","course_preferences","meeting_preferences"],"title":"FacultyProfileResponse"},"FacultyResponse":{"properties":{"nuid":{"type":"integer","title":"Nuid"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"campus":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Campus"},"active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Active"},"maxLoad":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Maxload"}},"type":"object","required":["nuid"],"title":"FacultyResponse"},"FacultyUpdate":{"properties":{"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"campus":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Campus"},"active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Active"},"max_load":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Load"}},"type":"object","title":"FacultyUpdate"},"GenerateScheduleRequest":{"properties":{"parameters":{"$ref":"#/components/schemas/AlgorithmParameters","default":{"MaxTimeBlockCapacity":0.15}}},"type":"object","title":"GenerateScheduleRequest"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InstructorInfo":{"properties":{"nuid":{"type":"integer","title":"Nuid"},"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"email":{"type":"string","title":"Email"},"course_preferences":{"items":{"$ref":"#/components/schemas/CoursePreferenceInfo"},"type":"array","title":"Course Preferences"},"meeting_preferences":{"items":{"$ref":"#/components/schemas/MeetingPreferenceInfo"},"type":"array","title":"Meeting Preferences"}},"type":"object","required":["nuid","first_name","last_name","email","course_preferences","meeting_preferences"],"title":"InstructorInfo"},"InviteLinkResponse":{"properties":{"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"email":{"type":"string","title":"Email"},"invite_link":{"type":"string","title":"Invite Link"}},"type":"object","required":["first_name","last_name","email","invite_link"],"title":"InviteLinkResponse"},"InviteRequest":{"properties":{"nuid":{"type":"integer","exclusiveMinimum":0.0,"title":"Nuid"},"role":{"type":"string","title":"Role","default":"VIEWER"}},"type":"object","required":["nuid"],"title":"InviteRequest"},"InviteResponse":{"properties":{"user":{"$ref":"#/components/schemas/UserResponse"},"signup_url":{"type":"string","title":"Signup Url"}},"type":"object","required":["user","signup_url"],"title":"InviteResponse"},"MeetingPreferenceInfo":{"properties":{"time_block_id":{"type":"integer","title":"Time Block Id"},"preference":{"type":"string","title":"Preference"}},"type":"object","required":["time_block_id","preference"],"title":"MeetingPreferenceInfo"},"RegenerateScheduleRequest":{"properties":{"parameters":{"$ref":"#/components/schemas/AlgorithmParameters","default":{"MaxTimeBlockCapacity":0.15}}},"type":"object","title":"RegenerateScheduleRequest"},"ScheduleActiveLockResponse":{"properties":{"section_id":{"type":"integer","title":"Section Id"},"locked_by":{"type":"integer","title":"Locked By"},"display_name":{"type":"string","title":"Display Name"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["section_id","locked_by","display_name","expires_at"],"title":"ScheduleActiveLockResponse","description":"Returned for each active lock when polling a schedule's lock state."},"ScheduleCreate":{"properties":{"name":{"type":"string","title":"Name"},"semester_id":{"type":"integer","title":"Semester Id"},"campus":{"type":"integer","title":"Campus"},"new_courses":{"items":{"type":"integer"},"type":"array","title":"New Courses","default":[]}},"type":"object","required":["name","semester_id","campus"],"title":"ScheduleCreate"},"ScheduleResponse":{"properties":{"schedule_id":{"type":"integer","title":"Schedule Id"},"name":{"type":"string","title":"Name"},"semester_id":{"type":"integer","title":"Semester Id"},"draft":{"type":"boolean","title":"Draft"},"campus":{"type":"integer","title":"Campus"},"active":{"type":"boolean","title":"Active"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","default":"idle"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"course_list":{"items":{"$ref":"#/components/schemas/CourseResponse"},"type":"array","title":"Course List","default":[]}},"type":"object","required":["schedule_id","name","semester_id","draft","campus","active"],"title":"ScheduleResponse"},"ScheduleUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"draft":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Draft"}},"type":"object","title":"ScheduleUpdate"},"SectionCreate":{"properties":{"schedule_id":{"type":"integer","title":"Schedule Id"},"course_id":{"type":"integer","title":"Course Id"},"time_block_id":{"type":"integer","title":"Time Block Id"},"capacity":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Capacity"},"faculty_nuids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Faculty Nuids"}},"type":"object","required":["schedule_id","course_id","time_block_id"],"title":"SectionCreate"},"SectionLockResponse":{"properties":{"section_lock_id":{"type":"integer","title":"Section Lock Id"},"section_id":{"type":"integer","title":"Section Id"},"locked_by":{"type":"integer","title":"Locked By"},"locked_at":{"type":"string","format":"date-time","title":"Locked At"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["section_lock_id","section_id","locked_by","locked_at","expires_at"],"title":"SectionLockResponse","description":"Returned on successful lock acquisition or refresh."},"SectionResponse":{"properties":{"section_id":{"type":"integer","title":"Section Id"},"schedule_id":{"type":"integer","title":"Schedule Id"},"time_block_id":{"type":"integer","title":"Time Block Id"},"course_id":{"type":"integer","title":"Course Id"},"capacity":{"type":"integer","title":"Capacity"},"section_number":{"type":"integer","title":"Section Number"},"room":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Room"},"assignment_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Assignment Score"}},"type":"object","required":["section_id","schedule_id","time_block_id","course_id","capacity","section_number"],"title":"SectionResponse"},"SectionRichResponse":{"properties":{"section_id":{"type":"integer","title":"Section Id"},"section_number":{"type":"integer","title":"Section Number"},"capacity":{"type":"integer","title":"Capacity"},"room":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Room"},"schedule_id":{"type":"integer","title":"Schedule Id"},"comment_count":{"type":"integer","title":"Comment Count","default":0},"crosslisted_section_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Crosslisted Section Id"},"course":{"$ref":"#/components/schemas/CourseInfo"},"time_block":{"anyOf":[{"$ref":"#/components/schemas/TimeBlockInfo"},{"type":"null"}]},"instructors":{"items":{"$ref":"#/components/schemas/InstructorInfo"},"type":"array","title":"Instructors"}},"type":"object","required":["section_id","section_number","capacity","schedule_id","course","instructors"],"title":"SectionRichResponse"},"SectionUpdate":{"properties":{"time_block_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Time Block Id"},"course_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Course Id"},"capacity":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Capacity"},"room":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Room"},"crosslisted_section_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Crosslisted Section Id"},"faculty_nuids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Faculty Nuids"}},"type":"object","title":"SectionUpdate"},"SemesterCreate":{"properties":{"season":{"type":"string","title":"Season"},"year":{"type":"integer","title":"Year"},"active":{"type":"boolean","title":"Active","default":true}},"type":"object","required":["season","year"],"title":"SemesterCreate"},"SemesterResponse":{"properties":{"semester_id":{"type":"integer","title":"Semester Id"},"season":{"type":"string","title":"Season"},"year":{"type":"integer","title":"Year"},"active":{"type":"boolean","title":"Active"}},"type":"object","required":["semester_id","season","year","active"],"title":"SemesterResponse"},"SemesterUpdate":{"properties":{"season":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Season"},"year":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year"},"active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Active"}},"type":"object","title":"SemesterUpdate"},"Severity":{"type":"integer","enum":[1,2,3],"title":"Severity"},"TimeBlockCreate":{"properties":{"meeting_days":{"type":"string","minLength":1,"title":"Meeting Days","description":"Day letters, e.g. 'MWF' or 'TR'"},"start_time":{"type":"string","title":"Start Time","description":"Start time in HH:MM format"},"end_time":{"type":"string","title":"End Time","description":"End time in HH:MM format"},"campus_id":{"type":"integer","title":"Campus Id"},"block_group":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Block Group","description":"8-character hex string linking two rows of a split block pair"}},"type":"object","required":["meeting_days","start_time","end_time","campus_id"],"title":"TimeBlockCreate","description":"Payload for creating a new time block.\n\n`meeting_days` should be a compact string of uppercase day letters,\ne.g. \"MWF\" for Monday/Wednesday/Friday or \"TR\" for Tuesday/Thursday.\n\n`start_time` and `end_time` must be in \"HH:MM\" 24-hour format.\n\n`block_group` is optional. Set it to the same 8-character hex string on\ntwo sibling rows to mark them as a split block (e.g. \"T 9:50–11:30\" and\n\"R 1:30–2:50\" both with the same block_group). Split blocks are excluded\nfrom auto-assignment and must be assigned manually."},"TimeBlockInfo":{"properties":{"time_block_id":{"type":"integer","title":"Time Block Id"},"days":{"type":"string","title":"Days"},"start_time":{"type":"string","title":"Start Time"},"end_time":{"type":"string","title":"End Time"}},"type":"object","required":["time_block_id","days","start_time","end_time"],"title":"TimeBlockInfo"},"TimeBlockResponse":{"properties":{"time_block_id":{"type":"integer","title":"Time Block Id"},"meeting_days":{"type":"string","title":"Meeting Days"},"start_time":{"type":"string","title":"Start Time"},"end_time":{"type":"string","title":"End Time"},"campus_id":{"type":"integer","title":"Campus Id"},"block_group":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Block Group"}},"type":"object","required":["time_block_id","meeting_days","start_time","end_time","campus_id"],"title":"TimeBlockResponse","description":"Full representation of a time block returned by the API."},"TimeBlockUpdate":{"properties":{"meeting_days":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meeting Days"},"start_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Start Time"},"end_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"End Time"},"campus_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Campus Id"},"block_group":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Block Group"}},"type":"object","title":"TimeBlockUpdate","description":"Partial update payload for a time block. All fields are optional —\nonly the fields included in the request body will be updated."},"UploadResponse":{"properties":{"status":{"type":"string","title":"Status"},"message":{"type":"string","title":"Message"},"records_processed":{"type":"integer","title":"Records Processed","default":0},"records_successful":{"type":"integer","title":"Records Successful","default":0},"records_failed":{"type":"integer","title":"Records Failed","default":0},"available_faculty":{"items":{"type":"integer"},"type":"array","title":"Available Faculty","default":[]},"errors":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Errors"}},"type":"object","required":["status","message"],"title":"UploadResponse"},"UserResponse":{"properties":{"user_id":{"type":"integer","title":"User Id"},"nuid":{"type":"integer","title":"Nuid"},"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"email":{"type":"string","title":"Email"},"role":{"type":"string","title":"Role"},"active":{"type":"boolean","title":"Active"}},"type":"object","required":["user_id","nuid","first_name","last_name","email","role","active"],"title":"UserResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"Warning":{"properties":{"Type":{"anyOf":[{"$ref":"#/components/schemas/WarningType"},{"type":"null"}],"description":"Type of warning"},"SeverityRank":{"$ref":"#/components/schemas/Severity","description":"Severity of this warning"},"Message":{"type":"string","title":"Message","description":"Warning detail for the user"},"FacultyID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Facultyid","description":"Related faculty member"},"CourseID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Courseid","description":"Related course"},"BlockID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Blockid","description":"Related time block"}},"type":"object","required":["SeverityRank","Message"],"title":"Warning"},"WarningResponse":{"properties":{"Type":{"anyOf":[{"$ref":"#/components/schemas/WarningType"},{"type":"null"}],"description":"Type of warning"},"SeverityRank":{"$ref":"#/components/schemas/Severity","description":"Severity of this warning"},"Message":{"type":"string","title":"Message","description":"Warning detail for the user"},"FacultyID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Facultyid","description":"Related faculty member"},"CourseID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Courseid","description":"Related course"},"BlockID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Blockid","description":"Related time block"},"warning_id":{"type":"integer","title":"Warning Id","description":"Unique warning ID"},"dismissed":{"type":"boolean","title":"Dismissed","description":"Whether this warning was dismissed","default":false},"section_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Section Id","description":"Directly linked section (manual-edit warnings only)"}},"type":"object","required":["SeverityRank","Message","warning_id"],"title":"WarningResponse","description":"Warning with persistence fields — returned by the API."},"WarningType":{"type":"string","enum":["Time block surpasses threshold","No valid time block for section-faculty pair","Faculty assigned unpreferenced course","Faculty assigned unpreferenced time","Conflict group courses overlap","Faculty overloaded with assignments","Insufficient faculty supply for section","Faculty double booked in time block"],"title":"WarningType","description":"Each member carries a human-readable message and a default severity.\n\nUsage:\n WarningType.FACULTY_OVERLOAD.value # → \"Faculty overloaded with assignments\"\n WarningType.FACULTY_OVERLOAD.severity # → Severity.HIGH"}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}}},"security":[{"BearerAuth":[]}]} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Automated Course Scheduler API","description":"API for the Automated Course Scheduler system","version":"1.0.0"},"paths":{"/sections":{"post":{"tags":["sections"],"summary":"Create Section","description":"Create a new section in a schedule.","operationId":"create_section_sections_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectionCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sections/{section_id}":{"patch":{"tags":["sections"],"summary":"Update Section","description":"Acquire (or refresh) the lock on this section, then apply the update.\n\nFails with 423 if another user currently holds the lock.\nBroadcasts section_updated to connected clients on success.","operationId":"update_section_sections__section_id__patch","parameters":[{"name":"section_id","in":"path","required":true,"schema":{"type":"integer","title":"Section Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectionUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sections"],"summary":"Delete Section","description":"Delete a section from a schedule.","operationId":"delete_section_sections__section_id__delete","parameters":[{"name":"section_id","in":"path","required":true,"schema":{"type":"integer","title":"Section Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules":{"get":{"tags":["schedules"],"summary":"Get Schedules","operationId":"get_schedules_schedules_get","parameters":[{"name":"campus_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Campus Id"}},{"name":"semester_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Semester Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleResponse"},"title":"Response Get Schedules Schedules Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["schedules"],"summary":"Create Schedule","description":"Create a new schedule draft.","operationId":"create_schedule_schedules_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}":{"get":{"tags":["schedules"],"summary":"Get Schedule","description":"Retrieve a specific schedule.","operationId":"get_schedule_schedules__schedule_id__get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["schedules"],"summary":"Update Schedule","description":"Update schedule metadata (name, complete status, etc.).","operationId":"update_schedule_schedules__schedule_id__put","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["schedules"],"summary":"Delete Schedule","description":"Delete a schedule and all its sections.","operationId":"delete_schedule_schedules__schedule_id__delete","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/sections":{"get":{"tags":["schedules"],"summary":"Get Schedule Sections","description":"Get all sections for a specific schedule.","operationId":"get_schedule_sections_schedules__schedule_id__sections_get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SectionResponse"},"title":"Response Get Schedule Sections Schedules Schedule Id Sections Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/sections/rich":{"get":{"tags":["schedules"],"summary":"Get Schedule Sections Rich","description":"Get all sections with denormalized course, time block, and instructor data.","operationId":"get_schedule_sections_rich_schedules__schedule_id__sections_rich_get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SectionRichResponse"},"title":"Response Get Schedule Sections Rich Schedules Schedule Id Sections Rich Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/export/csv":{"get":{"tags":["schedules"],"summary":"Export Schedule Csv","description":"Export a finalized schedule as a downloadable CSV.","operationId":"export_schedule_csv_schedules__schedule_id__export_csv_get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/locks":{"get":{"tags":["schedules"],"summary":"Get Schedule Locks","description":"Get all active locks for a schedule.\n\nArgs:\n schedule_id: ID of the schedule to query locks for.\n db: Database session.\n\nReturns:\n List of active locks with user display name included.","operationId":"get_schedule_locks_schedules__schedule_id__locks_get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleActiveLockResponse"},"title":"Response Get Schedule Locks Schedules Schedule Id Locks Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/semesters":{"get":{"tags":["semesters"],"summary":"Get All Semesters","description":"Get all semesters, with optional filtering by campus, semester name, or year.","operationId":"get_all_semesters_semesters_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/SemesterResponse"},"type":"array","title":"Response Get All Semesters Semesters Get"}}}}}},"post":{"tags":["semesters"],"summary":"Create Semester","description":"Create a new semester.","operationId":"create_semester_semesters_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemesterCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemesterCreate"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/semesters/{semester_id}":{"get":{"tags":["semesters"],"summary":"Get Semester","description":"Retrieve a specific semester.","operationId":"get_semester_semesters__semester_id__get","parameters":[{"name":"semester_id","in":"path","required":true,"schema":{"type":"integer","title":"Semester Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemesterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["semesters"],"summary":"Update Semester","description":"Update semester metadata (name, complete status, etc.).","operationId":"update_semester_semesters__semester_id__put","parameters":[{"name":"semester_id","in":"path","required":true,"schema":{"type":"integer","title":"Semester Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemesterUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemesterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["semesters"],"summary":"Delete Semester","description":"Delete a semester and all its sections.","operationId":"delete_semester_semesters__semester_id__delete","parameters":[{"name":"semester_id","in":"path","required":true,"schema":{"type":"integer","title":"Semester Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/courses":{"get":{"tags":["courses"],"summary":"Get Courses","description":"Retrieve all courses, optionally filtered by schedule","operationId":"get_courses_courses_get","parameters":[{"name":"schedule_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filter to courses with sections in this schedule","title":"Schedule Id"},"description":"Filter to courses with sections in this schedule"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CourseResponse"},"title":"Response Get Courses Courses Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["courses"],"summary":"Create Course","description":"Create a new course.","operationId":"create_course_courses_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/courses/{course_id}":{"get":{"tags":["courses"],"summary":"Get Course","description":"Retrieve a course by ID with section count.","operationId":"get_course_courses__course_id__get","parameters":[{"name":"course_id","in":"path","required":true,"schema":{"type":"integer","title":"Course Id"}},{"name":"schedule_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Section count for this schedule only","title":"Schedule Id"},"description":"Section count for this schedule only"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["courses"],"summary":"Update Course","description":"Partially update a course.","operationId":"update_course_courses__course_id__patch","parameters":[{"name":"course_id","in":"path","required":true,"schema":{"type":"integer","title":"Course Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["courses"],"summary":"Delete Course","description":"Delete a course with no sections.","operationId":"delete_course_courses__course_id__delete","parameters":[{"name":"course_id","in":"path","required":true,"schema":{"type":"integer","title":"Course Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/faculty":{"get":{"tags":["faculty"],"summary":"Get Faculty","description":"Retrieve all faculty members.","operationId":"get_faculty_faculty_get","parameters":[{"name":"campus","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by campus name","title":"Campus"},"description":"Filter by campus name"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Filter to only active faculty","default":false,"title":"Active Only"},"description":"Filter to only active faculty"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FacultyResponse"},"title":"Response Get Faculty Faculty Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["faculty"],"summary":"Create Faculty","description":"Create a new faculty member.","operationId":"create_faculty_faculty_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacultyCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacultyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/faculty/{nuid}":{"get":{"tags":["faculty"],"summary":"Get Faculty Profile","description":"Retrieve faculty profile with course and time preferences.","operationId":"get_faculty_profile_faculty__nuid__get","parameters":[{"name":"nuid","in":"path","required":true,"schema":{"type":"integer","title":"Nuid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacultyProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["faculty"],"summary":"Update Faculty","description":"Partially update faculty demographics and status.","operationId":"update_faculty_faculty__nuid__patch","parameters":[{"name":"nuid","in":"path","required":true,"schema":{"type":"integer","title":"Nuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacultyUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacultyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["faculty"],"summary":"Delete Faculty","description":"Delete a faculty member and their preferences and assignments.","operationId":"delete_faculty_faculty__nuid__delete","parameters":[{"name":"nuid","in":"path","required":true,"schema":{"type":"integer","title":"Nuid"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/faculty/build_profiles":{"post":{"tags":["faculty"],"summary":"Build Profiles","operationId":"build_profiles_faculty_build_profiles_post","requestBody":{"content":{"application/json":{"schema":{"items":{"type":"integer"},"type":"array","title":"Available Faculty"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/FacultyProfileResponse"},"type":"array","title":"Response Build Profiles Faculty Build Profiles Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/time-blocks":{"get":{"tags":["time-blocks"],"summary":"Get Time Blocks","description":"Retrieve all time blocks, optionally filtered by campus.","operationId":"get_time_blocks_time_blocks_get","parameters":[{"name":"campus_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filter by campus ID","title":"Campus Id"},"description":"Filter by campus ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TimeBlockResponse"},"title":"Response Get Time Blocks Time Blocks Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["time-blocks"],"summary":"Create Time Block","description":"Create a new time block.\n\n`meeting_days` should be a compact uppercase day string (e.g. \"MWF\", \"TR\").\n`start_time` and `end_time` must be in HH:MM format.\nSet `block_group` to the same 8-character hex string on two rows to mark\nthem as a split block pair — split blocks are excluded from auto-assignment.\nReturns 409 if the block_group already has a complete pair on this campus.","operationId":"create_time_block_time_blocks_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeBlockCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeBlockResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/time-blocks/{time_block_id}":{"patch":{"tags":["time-blocks"],"summary":"Update Time Block","description":"Partially update a time block. Only fields present in the request body are changed.","operationId":"update_time_block_time_blocks__time_block_id__patch","parameters":[{"name":"time_block_id","in":"path","required":true,"schema":{"type":"integer","title":"Time Block Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeBlockUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeBlockResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["time-blocks"],"summary":"Delete Time Block","description":"Delete a time block.\n\nReturns 400 if any sections are currently assigned to this block —\nthose sections must be reassigned or removed first.","operationId":"delete_time_block_time_blocks__time_block_id__delete","parameters":[{"name":"time_block_id","in":"path","required":true,"schema":{"type":"integer","title":"Time Block Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/campuses":{"get":{"tags":["campuses"],"summary":"Get All Campuses","description":"Retrieve all campuses, optionally filtered by name.","operationId":"get_all_campuses_campuses_get","parameters":[{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CampusResponse"},"title":"Response Get All Campuses Campuses Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["campuses"],"summary":"Create Campus","description":"Create a new campus.","operationId":"create_campus_campuses_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampusCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/campuses/{campus_id}":{"get":{"tags":["campuses"],"summary":"Get Campus","description":"Retrieve a specific campus by ID.","operationId":"get_campus_campuses__campus_id__get","parameters":[{"name":"campus_id","in":"path","required":true,"schema":{"type":"integer","title":"Campus Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["campuses"],"summary":"Update Campus","description":"Update campus metadata (name, etc.).","operationId":"update_campus_campuses__campus_id__put","parameters":[{"name":"campus_id","in":"path","required":true,"schema":{"type":"integer","title":"Campus Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampusUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["campuses"],"summary":"Delete Campus","description":"Delete a campus.","operationId":"delete_campus_campuses__campus_id__delete","parameters":[{"name":"campus_id","in":"path","required":true,"schema":{"type":"integer","title":"Campus Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/upload/courses":{"post":{"tags":["upload"],"summary":"Upload Courses","description":"Upload a CSV file containing course offering data.","operationId":"upload_courses_upload_courses_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_courses_upload_courses_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/upload/faculty-preferences":{"post":{"tags":["upload"],"summary":"Upload Faculty Preferences","description":"Upload a CSV file containing faculty preference data.","operationId":"upload_faculty_preferences_upload_faculty_preferences_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_faculty_preferences_upload_faculty_preferences_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/upload/time-preferences":{"post":{"tags":["upload"],"summary":"Upload Time Preferences","description":"Upload a CSV file containing faculty time preference data.","operationId":"upload_time_preferences_upload_time_preferences_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_time_preferences_upload_time_preferences_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/comments":{"post":{"tags":["comments"],"summary":"Post Comment","operationId":"post_comment_comments_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentSchema"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/comments/{parent_id}":{"post":{"tags":["comments"],"summary":"Post Reply","operationId":"post_reply_comments__parent_id__post","parameters":[{"name":"parent_id","in":"path","required":true,"schema":{"type":"integer","title":"Parent Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentSchema"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/comments/{section_id}":{"get":{"tags":["comments"],"summary":"Get Comments","operationId":"get_comments_comments__section_id__get","parameters":[{"name":"section_id","in":"path","required":true,"schema":{"type":"integer","title":"Section Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CommentResponse"},"title":"Response Get Comments Comments Section Id Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/comments/{comment_id}":{"delete":{"tags":["comments"],"summary":"Delete Comment","operationId":"delete_comment_comments__comment_id__delete","parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"integer","title":"Comment Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["comments"],"summary":"Resolve Comment","operationId":"resolve_comment_comments__comment_id__put","parameters":[{"name":"comment_id","in":"path","required":true,"schema":{"type":"integer","title":"Comment Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sections/{section_id}/lock":{"post":{"tags":["section_locks"],"summary":"Acquire Lock","description":"Acquire a lock on a section for editing.\n\nRaises:\n HTTPException: 403 if the caller does not have the admin role.\n HTTPException: 423 if the section is locked by another user.","operationId":"acquire_lock_sections__section_id__lock_post","parameters":[{"name":"section_id","in":"path","required":true,"schema":{"type":"integer","title":"Section Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectionLockResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sections/{section_id}/unlock":{"post":{"tags":["section_locks"],"summary":"Release Lock","description":"Release a lock on a section.\n\nRaises:\n HTTPException: 403 if the caller does not own an active lock.","operationId":"release_lock_sections__section_id__unlock_post","parameters":[{"name":"section_id","in":"path","required":true,"schema":{"type":"integer","title":"Section Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/warnings":{"get":{"tags":["warnings"],"summary":"Get Schedule Warnings","operationId":"get_schedule_warnings_schedules__schedule_id__warnings_get","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}},{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type"}},{"name":"severity","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Severity"}},{"name":"include_dismissed","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Dismissed"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WarningResponse"},"title":"Response Get Schedule Warnings Schedules Schedule Id Warnings Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["warnings"],"summary":"Create Warning","operationId":"create_warning_schedules__schedule_id__warnings_post","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Warning"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WarningResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/warnings/{warning_id}/dismiss":{"patch":{"tags":["warnings"],"summary":"Dismiss Warning","operationId":"dismiss_warning_schedules__schedule_id__warnings__warning_id__dismiss_patch","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}},{"name":"warning_id","in":"path","required":true,"schema":{"type":"integer","title":"Warning Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/warnings/{warning_id}/restore":{"patch":{"tags":["warnings"],"summary":"Restore Warning","operationId":"restore_warning_schedules__schedule_id__warnings__warning_id__restore_patch","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}},{"name":"warning_id","in":"path","required":true,"schema":{"type":"integer","title":"Warning Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/warnings/{warning_id}":{"delete":{"tags":["warnings"],"summary":"Delete Warning","operationId":"delete_warning_schedules__schedule_id__warnings__warning_id__delete","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}},{"name":"warning_id","in":"path","required":true,"schema":{"type":"integer","title":"Warning Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/generate":{"post":{"tags":["schedules"],"summary":"Run Algorithm","operationId":"run_algorithm_schedules__schedule_id__generate_post","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateScheduleRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/schedules/{schedule_id}/regenerate":{"post":{"tags":["schedules"],"summary":"Regenerate Algorithm","operationId":"regenerate_algorithm_schedules__schedule_id__regenerate_post","parameters":[{"name":"schedule_id","in":"path","required":true,"schema":{"type":"integer","title":"Schedule Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegenerateScheduleRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/invites":{"post":{"tags":["users"],"summary":"Create Invite","description":"Invite a faculty member. Requires admin role.\n\nLooks up the faculty record by NUID, creates a User, registers them in\nAuth0, generates a password-change ticket, and sends an invite email.","operationId":"create_invite_api_invites_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/invites/admin":{"post":{"tags":["users"],"summary":"Create Admin Invite","description":"Create a pending admin from NUID and name/email (no faculty record). Requires admin.","operationId":"create_admin_invite_api_invites_admin_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminInviteRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/invites/export":{"get":{"tags":["users"],"summary":"Export Invites","description":"Return invite links for all active faculty without a linked account.\n\nCreates pending User records for any who were not yet invited.\nRequires admin role.","operationId":"export_invites_api_invites_export_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/InviteLinkResponse"},"type":"array","title":"Response Export Invites Api Invites Export Get"}}}}}}},"/api/users":{"get":{"tags":["users"],"summary":"List Users","description":"Return all users. Requires admin role.","operationId":"list_users_api_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/UserResponse"},"type":"array","title":"Response List Users Api Users Get"}}}}}}},"/api/users/me":{"get":{"tags":["users"],"summary":"Get Me","description":"Return the profile of the currently authenticated user.","operationId":"get_me_api_users_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}}}},"/api/users/{user_id}":{"get":{"tags":["users"],"summary":"Get User","description":"Return a single user by ID. Requires admin role.","operationId":"get_user_api_users__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AdminInviteRequest":{"properties":{"nuid":{"type":"integer","exclusiveMinimum":0.0,"title":"Nuid"},"first_name":{"type":"string","maxLength":100,"minLength":1,"title":"First Name"},"last_name":{"type":"string","maxLength":100,"minLength":1,"title":"Last Name"},"email":{"type":"string","maxLength":100,"minLength":3,"title":"Email"}},"type":"object","required":["nuid","first_name","last_name","email"],"title":"AdminInviteRequest","description":"Pending admin user + Auth0 signup URL (no faculty row; same linking as bootstrap_admin)."},"AlgorithmParameters":{"properties":{"MaxTimeBlockCapacity":{"type":"number","maximum":1.0,"exclusiveMinimum":0.0,"title":"Maxtimeblockcapacity","description":"Max department percentage per time block","default":0.15}},"type":"object","title":"AlgorithmParameters"},"Body_upload_courses_upload_courses_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_courses_upload_courses_post"},"Body_upload_faculty_preferences_upload_faculty_preferences_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_faculty_preferences_upload_faculty_preferences_post"},"Body_upload_time_preferences_upload_time_preferences_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_time_preferences_upload_time_preferences_post"},"CampusCreate":{"properties":{"name":{"type":"string","title":"Name"},"active":{"type":"boolean","title":"Active","default":true}},"type":"object","required":["name"],"title":"CampusCreate"},"CampusResponse":{"properties":{"campus_id":{"type":"integer","title":"Campus Id"},"name":{"type":"string","title":"Name"},"active":{"type":"boolean","title":"Active"}},"type":"object","required":["campus_id","name","active"],"title":"CampusResponse"},"CampusUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Active"}},"type":"object","title":"CampusUpdate"},"CommentResponse":{"properties":{"comment_id":{"type":"integer","title":"Comment Id"},"user_id":{"type":"integer","title":"User Id"},"section_id":{"type":"integer","title":"Section Id"},"parent_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Id"},"content":{"type":"string","title":"Content"},"resolved":{"type":"boolean","title":"Resolved"},"active":{"type":"boolean","title":"Active"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"user":{"$ref":"#/components/schemas/CommentUserInfo"}},"type":"object","required":["comment_id","user_id","section_id","parent_id","content","resolved","active","created_at","user"],"title":"CommentResponse"},"CommentSchema":{"properties":{"section_id":{"type":"integer","title":"Section Id"},"user_id":{"type":"integer","title":"User Id"},"content":{"type":"string","title":"Content"}},"type":"object","required":["section_id","user_id","content"],"title":"CommentSchema"},"CommentUserInfo":{"properties":{"user_id":{"type":"integer","title":"User Id"},"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"email":{"type":"string","title":"Email"}},"type":"object","required":["user_id","first_name","last_name","email"],"title":"CommentUserInfo"},"CourseCreate":{"properties":{"subject":{"type":"string","maxLength":10,"minLength":1,"title":"Subject"},"code":{"type":"integer","exclusiveMinimum":0.0,"title":"Code"},"name":{"type":"string","minLength":1,"title":"Name"},"description":{"type":"string","minLength":1,"title":"Description"},"credits":{"type":"integer","minimum":0.0,"title":"Credits"},"priority":{"type":"boolean","title":"Priority","default":false}},"type":"object","required":["subject","code","name","description","credits"],"title":"CourseCreate"},"CourseInfo":{"properties":{"course_id":{"type":"integer","title":"Course Id"},"subject":{"type":"string","title":"Subject"},"code":{"type":"integer","title":"Code"},"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"credits":{"type":"integer","title":"Credits"}},"type":"object","required":["course_id","subject","code","name","description","credits"],"title":"CourseInfo"},"CoursePreferenceInfo":{"properties":{"course_id":{"type":"integer","title":"Course Id"},"course_name":{"type":"string","title":"Course Name"},"preference":{"type":"string","title":"Preference"}},"type":"object","required":["course_id","course_name","preference"],"title":"CoursePreferenceInfo"},"CourseResponse":{"properties":{"course_id":{"type":"integer","title":"Course Id"},"subject":{"type":"string","title":"Subject"},"code":{"type":"integer","title":"Code"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"credits":{"type":"integer","title":"Credits"},"priority":{"type":"boolean","title":"Priority","default":false},"section_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Section Count"},"qualified_faculty":{"type":"integer","title":"Qualified Faculty","default":0}},"type":"object","required":["course_id","subject","code","name","credits"],"title":"CourseResponse"},"CourseUpdate":{"properties":{"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Code"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"credits":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Credits"},"priority":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Priority"}},"type":"object","title":"CourseUpdate"},"FacultyCreate":{"properties":{"nuid":{"type":"integer","exclusiveMinimum":0.0,"title":"Nuid"},"first_name":{"type":"string","minLength":1,"title":"First Name"},"last_name":{"type":"string","minLength":1,"title":"Last Name"},"email":{"type":"string","minLength":1,"title":"Email"},"campus":{"type":"integer","exclusiveMinimum":0.0,"title":"Campus"},"active":{"type":"boolean","title":"Active","default":true},"max_load":{"type":"integer","minimum":1.0,"title":"Max Load","default":3}},"type":"object","required":["nuid","first_name","last_name","email","campus"],"title":"FacultyCreate"},"FacultyProfileResponse":{"properties":{"nuid":{"type":"integer","title":"Nuid"},"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"email":{"type":"string","title":"Email"},"campus":{"type":"integer","title":"Campus"},"active":{"type":"boolean","title":"Active"},"maxLoad":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Maxload"},"needsAdminReview":{"type":"boolean","title":"Needsadminreview","default":false},"course_preferences":{"items":{"$ref":"#/components/schemas/CoursePreferenceInfo"},"type":"array","title":"Course Preferences"},"meeting_preferences":{"items":{"$ref":"#/components/schemas/MeetingPreferenceInfo"},"type":"array","title":"Meeting Preferences"}},"type":"object","required":["nuid","first_name","last_name","email","campus","active","course_preferences","meeting_preferences"],"title":"FacultyProfileResponse"},"FacultyResponse":{"properties":{"nuid":{"type":"integer","title":"Nuid"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"campus":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Campus"},"active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Active"},"maxLoad":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Maxload"}},"type":"object","required":["nuid"],"title":"FacultyResponse"},"FacultyUpdate":{"properties":{"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"campus":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Campus"},"active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Active"},"max_load":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Load"}},"type":"object","title":"FacultyUpdate"},"GenerateScheduleRequest":{"properties":{"parameters":{"$ref":"#/components/schemas/AlgorithmParameters","default":{"MaxTimeBlockCapacity":0.15}}},"type":"object","title":"GenerateScheduleRequest"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InstructorInfo":{"properties":{"nuid":{"type":"integer","title":"Nuid"},"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"email":{"type":"string","title":"Email"},"course_preferences":{"items":{"$ref":"#/components/schemas/CoursePreferenceInfo"},"type":"array","title":"Course Preferences"},"meeting_preferences":{"items":{"$ref":"#/components/schemas/MeetingPreferenceInfo"},"type":"array","title":"Meeting Preferences"}},"type":"object","required":["nuid","first_name","last_name","email","course_preferences","meeting_preferences"],"title":"InstructorInfo"},"InviteLinkResponse":{"properties":{"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"email":{"type":"string","title":"Email"},"invite_link":{"type":"string","title":"Invite Link"}},"type":"object","required":["first_name","last_name","email","invite_link"],"title":"InviteLinkResponse"},"InviteRequest":{"properties":{"nuid":{"type":"integer","exclusiveMinimum":0.0,"title":"Nuid"},"role":{"type":"string","title":"Role","default":"VIEWER"}},"type":"object","required":["nuid"],"title":"InviteRequest"},"InviteResponse":{"properties":{"user":{"$ref":"#/components/schemas/UserResponse"},"signup_url":{"type":"string","title":"Signup Url"}},"type":"object","required":["user","signup_url"],"title":"InviteResponse"},"MeetingPreferenceInfo":{"properties":{"time_block_id":{"type":"integer","title":"Time Block Id"},"preference":{"type":"string","title":"Preference"}},"type":"object","required":["time_block_id","preference"],"title":"MeetingPreferenceInfo"},"RegenerateScheduleRequest":{"properties":{"parameters":{"$ref":"#/components/schemas/AlgorithmParameters","default":{"MaxTimeBlockCapacity":0.15}}},"type":"object","title":"RegenerateScheduleRequest"},"ScheduleActiveLockResponse":{"properties":{"section_id":{"type":"integer","title":"Section Id"},"locked_by":{"type":"integer","title":"Locked By"},"display_name":{"type":"string","title":"Display Name"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["section_id","locked_by","display_name","expires_at"],"title":"ScheduleActiveLockResponse","description":"Returned for each active lock when polling a schedule's lock state."},"ScheduleCreate":{"properties":{"name":{"type":"string","title":"Name"},"semester_id":{"type":"integer","title":"Semester Id"},"campus":{"type":"integer","title":"Campus"},"new_courses":{"items":{"type":"integer"},"type":"array","title":"New Courses","default":[]}},"type":"object","required":["name","semester_id","campus"],"title":"ScheduleCreate"},"ScheduleResponse":{"properties":{"schedule_id":{"type":"integer","title":"Schedule Id"},"name":{"type":"string","title":"Name"},"semester_id":{"type":"integer","title":"Semester Id"},"draft":{"type":"boolean","title":"Draft"},"campus":{"type":"integer","title":"Campus"},"active":{"type":"boolean","title":"Active"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","default":"idle"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"course_list":{"items":{"$ref":"#/components/schemas/CourseResponse"},"type":"array","title":"Course List","default":[]}},"type":"object","required":["schedule_id","name","semester_id","draft","campus","active"],"title":"ScheduleResponse"},"ScheduleUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"draft":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Draft"}},"type":"object","title":"ScheduleUpdate"},"SectionCreate":{"properties":{"schedule_id":{"type":"integer","title":"Schedule Id"},"course_id":{"type":"integer","title":"Course Id"},"time_block_id":{"type":"integer","title":"Time Block Id"},"capacity":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Capacity"},"faculty_nuids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Faculty Nuids"}},"type":"object","required":["schedule_id","course_id","time_block_id"],"title":"SectionCreate"},"SectionLockResponse":{"properties":{"section_lock_id":{"type":"integer","title":"Section Lock Id"},"section_id":{"type":"integer","title":"Section Id"},"locked_by":{"type":"integer","title":"Locked By"},"locked_at":{"type":"string","format":"date-time","title":"Locked At"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["section_lock_id","section_id","locked_by","locked_at","expires_at"],"title":"SectionLockResponse","description":"Returned on successful lock acquisition or refresh."},"SectionResponse":{"properties":{"section_id":{"type":"integer","title":"Section Id"},"schedule_id":{"type":"integer","title":"Schedule Id"},"time_block_id":{"type":"integer","title":"Time Block Id"},"course_id":{"type":"integer","title":"Course Id"},"capacity":{"type":"integer","title":"Capacity"},"section_number":{"type":"integer","title":"Section Number"},"room":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Room"},"assignment_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Assignment Score"}},"type":"object","required":["section_id","schedule_id","time_block_id","course_id","capacity","section_number"],"title":"SectionResponse"},"SectionRichResponse":{"properties":{"section_id":{"type":"integer","title":"Section Id"},"section_number":{"type":"integer","title":"Section Number"},"capacity":{"type":"integer","title":"Capacity"},"room":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Room"},"schedule_id":{"type":"integer","title":"Schedule Id"},"comment_count":{"type":"integer","title":"Comment Count","default":0},"crosslisted_section_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Crosslisted Section Id"},"course":{"$ref":"#/components/schemas/CourseInfo"},"time_block":{"anyOf":[{"$ref":"#/components/schemas/TimeBlockInfo"},{"type":"null"}]},"instructors":{"items":{"$ref":"#/components/schemas/InstructorInfo"},"type":"array","title":"Instructors"}},"type":"object","required":["section_id","section_number","capacity","schedule_id","course","instructors"],"title":"SectionRichResponse"},"SectionUpdate":{"properties":{"time_block_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Time Block Id"},"course_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Course Id"},"capacity":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Capacity"},"room":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Room"},"crosslisted_section_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Crosslisted Section Id"},"faculty_nuids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Faculty Nuids"}},"type":"object","title":"SectionUpdate"},"SemesterCreate":{"properties":{"season":{"type":"string","title":"Season"},"year":{"type":"integer","title":"Year"},"active":{"type":"boolean","title":"Active","default":true}},"type":"object","required":["season","year"],"title":"SemesterCreate"},"SemesterResponse":{"properties":{"semester_id":{"type":"integer","title":"Semester Id"},"season":{"type":"string","title":"Season"},"year":{"type":"integer","title":"Year"},"active":{"type":"boolean","title":"Active"}},"type":"object","required":["semester_id","season","year","active"],"title":"SemesterResponse"},"SemesterUpdate":{"properties":{"season":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Season"},"year":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year"},"active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Active"}},"type":"object","title":"SemesterUpdate"},"Severity":{"type":"integer","enum":[1,2,3],"title":"Severity"},"TimeBlockCreate":{"properties":{"meeting_days":{"type":"string","minLength":1,"title":"Meeting Days","description":"Day letters, e.g. 'MWF' or 'TR'"},"start_time":{"type":"string","title":"Start Time","description":"Start time in HH:MM format"},"end_time":{"type":"string","title":"End Time","description":"End time in HH:MM format"},"campus_id":{"type":"integer","title":"Campus Id"},"block_group":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Block Group","description":"8-character hex string linking two rows of a split block pair"}},"type":"object","required":["meeting_days","start_time","end_time","campus_id"],"title":"TimeBlockCreate","description":"Payload for creating a new time block.\n\n`meeting_days` should be a compact string of uppercase day letters,\ne.g. \"MWF\" for Monday/Wednesday/Friday or \"TR\" for Tuesday/Thursday.\n\n`start_time` and `end_time` must be in \"HH:MM\" 24-hour format.\n\n`block_group` is optional. Set it to the same 8-character hex string on\ntwo sibling rows to mark them as a split block (e.g. \"T 9:50–11:30\" and\n\"R 1:30–2:50\" both with the same block_group). Split blocks are excluded\nfrom auto-assignment and must be assigned manually."},"TimeBlockInfo":{"properties":{"time_block_id":{"type":"integer","title":"Time Block Id"},"days":{"type":"string","title":"Days"},"start_time":{"type":"string","title":"Start Time"},"end_time":{"type":"string","title":"End Time"}},"type":"object","required":["time_block_id","days","start_time","end_time"],"title":"TimeBlockInfo"},"TimeBlockResponse":{"properties":{"time_block_id":{"type":"integer","title":"Time Block Id"},"meeting_days":{"type":"string","title":"Meeting Days"},"start_time":{"type":"string","title":"Start Time"},"end_time":{"type":"string","title":"End Time"},"campus_id":{"type":"integer","title":"Campus Id"},"block_group":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Block Group"}},"type":"object","required":["time_block_id","meeting_days","start_time","end_time","campus_id"],"title":"TimeBlockResponse","description":"Full representation of a time block returned by the API."},"TimeBlockUpdate":{"properties":{"meeting_days":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Meeting Days"},"start_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Start Time"},"end_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"End Time"},"campus_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Campus Id"},"block_group":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Block Group"}},"type":"object","title":"TimeBlockUpdate","description":"Partial update payload for a time block. All fields are optional —\nonly the fields included in the request body will be updated."},"UploadResponse":{"properties":{"status":{"type":"string","title":"Status"},"message":{"type":"string","title":"Message"},"records_processed":{"type":"integer","title":"Records Processed","default":0},"records_successful":{"type":"integer","title":"Records Successful","default":0},"records_failed":{"type":"integer","title":"Records Failed","default":0},"available_faculty":{"items":{"type":"integer"},"type":"array","title":"Available Faculty","default":[]},"errors":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Errors"}},"type":"object","required":["status","message"],"title":"UploadResponse"},"UserResponse":{"properties":{"user_id":{"type":"integer","title":"User Id"},"nuid":{"type":"integer","title":"Nuid"},"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"email":{"type":"string","title":"Email"},"role":{"type":"string","title":"Role"},"active":{"type":"boolean","title":"Active"}},"type":"object","required":["user_id","nuid","first_name","last_name","email","role","active"],"title":"UserResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"Warning":{"properties":{"Type":{"anyOf":[{"$ref":"#/components/schemas/WarningType"},{"type":"null"}],"description":"Type of warning"},"SeverityRank":{"$ref":"#/components/schemas/Severity","description":"Severity of this warning"},"Message":{"type":"string","title":"Message","description":"Warning detail for the user"},"FacultyID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Facultyid","description":"Related faculty member"},"CourseID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Courseid","description":"Related course"},"BlockID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Blockid","description":"Related time block"}},"type":"object","required":["SeverityRank","Message"],"title":"Warning"},"WarningResponse":{"properties":{"Type":{"anyOf":[{"$ref":"#/components/schemas/WarningType"},{"type":"null"}],"description":"Type of warning"},"SeverityRank":{"$ref":"#/components/schemas/Severity","description":"Severity of this warning"},"Message":{"type":"string","title":"Message","description":"Warning detail for the user"},"FacultyID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Facultyid","description":"Related faculty member"},"CourseID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Courseid","description":"Related course"},"BlockID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Blockid","description":"Related time block"},"warning_id":{"type":"integer","title":"Warning Id","description":"Unique warning ID"},"dismissed":{"type":"boolean","title":"Dismissed","description":"Whether this warning was dismissed","default":false},"dismissed_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dismissed By","description":"Display name of the user who dismissed the warning"},"section_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Section Id","description":"Directly linked section (manual-edit warnings only)"}},"type":"object","required":["SeverityRank","Message","warning_id"],"title":"WarningResponse","description":"Warning with persistence fields — returned by the API."},"WarningType":{"type":"string","enum":["Time block surpasses threshold","No valid time block for section-faculty pair","Faculty assigned unpreferenced course","Faculty assigned unpreferenced time","Conflict group courses overlap","Faculty overloaded with assignments","Insufficient faculty supply for section","Faculty double booked in time block"],"title":"WarningType","description":"Each member carries a human-readable message and a default severity.\n\nUsage:\n WarningType.FACULTY_OVERLOAD.value # → \"Faculty overloaded with assignments\"\n WarningType.FACULTY_OVERLOAD.severity # → Severity.HIGH"}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}}},"security":[{"BearerAuth":[]}]} \ No newline at end of file diff --git a/frontend/src/api/generated.ts b/frontend/src/api/generated.ts index 7237658..f0b4d34 100644 --- a/frontend/src/api/generated.ts +++ b/frontend/src/api/generated.ts @@ -603,6 +603,11 @@ export type WarningResponseCourseID = number | null; */ export type WarningResponseBlockID = number | null; +/** + * Display name of the user who dismissed the warning + */ +export type WarningResponseDismissedBy = string | null; + /** * Directly linked section (manual-edit warnings only) */ @@ -628,6 +633,8 @@ export interface WarningResponse { warning_id: number; /** Whether this warning was dismissed */ dismissed?: boolean; + /** Display name of the user who dismissed the warning */ + dismissed_by?: WarningResponseDismissedBy; /** Directly linked section (manual-edit warnings only) */ section_id?: WarningResponseSectionId; } diff --git a/frontend/src/components/ScheduleSectionRowView.tsx b/frontend/src/components/ScheduleSectionRowView.tsx index 85f7230..60a5f3f 100644 --- a/frontend/src/components/ScheduleSectionRowView.tsx +++ b/frontend/src/components/ScheduleSectionRowView.tsx @@ -662,7 +662,7 @@ export default function ScheduleSectionRowView({ mode="edit" scheduleId={scheduleId} section={editingSection} - warnings={warnings.filter((w) => w.section_id === editingSection.section_id && !w.dismissed)} + warnings={warnings.filter((w) => w.section_id === editingSection.section_id)} timeBlocks={timeBlocks} campusId={campusId} campusName={campusName} diff --git a/frontend/src/components/SectionMutationDrawer.tsx b/frontend/src/components/SectionMutationDrawer.tsx index b370841..2d38448 100644 --- a/frontend/src/components/SectionMutationDrawer.tsx +++ b/frontend/src/components/SectionMutationDrawer.tsx @@ -501,6 +501,34 @@ export default function SectionMutationDrawer(props: Props) { const isEdit = props.mode === 'edit'; const section = isEdit ? props.section : null; const sectionWarnings: WarningResponse[] = isEdit ? (props.warnings ?? []) : []; + const activeWarnings = sectionWarnings.filter((w) => !w.dismissed); + const dismissedWarnings = sectionWarnings.filter((w) => w.dismissed); + const [showDismissed, setShowDismissed] = useState(false); + const [pendingWarningId, setPendingWarningId] = useState(null); + + async function handleDismissWarning(w: WarningResponse) { + setPendingWarningId(w.warning_id); + try { + await getAutomatedCourseSchedulerAPI().dismissWarningSchedulesScheduleIdWarningsWarningIdDismissPatch( + scheduleId, + w.warning_id, + ); + } finally { + setPendingWarningId(null); + } + } + + async function handleRestoreWarning(w: WarningResponse) { + setPendingWarningId(w.warning_id); + try { + await getAutomatedCourseSchedulerAPI().restoreWarningSchedulesScheduleIdWarningsWarningIdRestorePatch( + scheduleId, + w.warning_id, + ); + } finally { + setPendingWarningId(null); + } + } const [originalCrosslistedId, setOriginalCrosslistedId] = useState( section?.crosslisted_section_id ?? null, ); @@ -1011,7 +1039,7 @@ export default function SectionMutationDrawer(props: Props) {

Warnings

- {sectionWarnings.map((w) => { + {activeWarnings.map((w) => { const sev = w.SeverityRank; const badgeClass = sev === Severity.NUMBER_3 @@ -1020,14 +1048,62 @@ export default function SectionMutationDrawer(props: Props) { ? 'text-amber-700 bg-amber-50 border border-amber-200' : 'text-yellow-700 bg-yellow-50 border border-yellow-200'; const label = sev === Severity.NUMBER_3 ? 'High' : sev === Severity.NUMBER_2 ? 'Medium' : 'Low'; + const busy = pendingWarningId === w.warning_id; return (
- {w.Message} - {label} + {w.Message} +
+ {label} + +
); })}
+ + {dismissedWarnings.length > 0 && ( +
+ + {showDismissed && ( +
+ {dismissedWarnings.map((w) => { + const busy = pendingWarningId === w.warning_id; + return ( +
+ + {w.Message} + {w.dismissed_by && ( + · dismissed by {w.dismissed_by} + )} + + +
+ ); + })} +
+ )} +
+ )}
)} diff --git a/frontend/src/stores/scheduleDataStore.ts b/frontend/src/stores/scheduleDataStore.ts index 665564c..30d454f 100644 --- a/frontend/src/stores/scheduleDataStore.ts +++ b/frontend/src/stores/scheduleDataStore.ts @@ -73,7 +73,7 @@ function fetchInitialSnapshot(scheduleId: number) { }) .catch(() => {}); api - .getScheduleWarningsSchedulesScheduleIdWarningsGet(scheduleId) + .getScheduleWarningsSchedulesScheduleIdWarningsGet(scheduleId, { include_dismissed: true }) .then((list) => { if (currentScheduleId !== scheduleId) return; useScheduleDataStore.setState({ warnings: list }); @@ -157,7 +157,7 @@ function handleMessage(msg: { type: string; payload: unknown }, scheduleId: numb } case 'section_warnings': { getAutomatedCourseSchedulerAPI() - .getScheduleWarningsSchedulesScheduleIdWarningsGet(scheduleId) + .getScheduleWarningsSchedulesScheduleIdWarningsGet(scheduleId, { include_dismissed: true }) .then((list) => { if (currentScheduleId !== scheduleId) return; useScheduleDataStore.setState({ warnings: list });