Miscellaneous fixes (#21141)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled

* Remove source_type from API

* Don't require state classification models to select all classes

* Specifically validate provided end_time for manual events

* Remove yolov9 specification for warning

* Remove warning for coral

* clarify zone name tip

* clarify replace rules in lpr docs

* remove periods

* Add explanation for review report

* adjust HLS gap controller params

defaults to false, should help to recover from hangs and stalling in tracking details videos on chrome

* only redirect to login page once on 401

attempt to fix ios pwa safari redirect storm

* Use contextual information from other cameras to inform report summary

* Formatting and prompt improvements for review summary report

* More improvements to prompt

* Remove examples

* Don't show admin action buttons on export card

* fix redirect race condition

Coordinate 401 redirect logic between ApiProvider and ProtectedRoute using a shared flag to prevent multiple simultaneous redirects that caused UI flashing. Ensure both auth error paths check and set the redirect flag before navigating to login, eliminating race conditions where both mechanisms could trigger at once

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
Nicolas Mowen
2025-12-04 12:19:07 -06:00
committed by GitHub
co-authored by Josh Hawkins
parent 9ab78f496c
commit c136e5e8bd
17 changed files with 798 additions and 677 deletions
-1
View File
@@ -29,7 +29,6 @@ class EventsDescriptionBody(BaseModel):
class EventsCreateBody(BaseModel):
source_type: Optional[str] = "api"
sub_label: Optional[str] = None
score: Optional[float] = 0
duration: Optional[int] = 30
+38 -21
View File
@@ -346,7 +346,7 @@ def events(
"/events/explore",
response_model=list[EventResponse],
dependencies=[Depends(allow_any_authenticated())],
summary="Get summary of objects.",
summary="Get summary of objects",
description="""Gets a summary of objects from the database.
Returns a list of objects with a max of `limit` objects for each label.
""",
@@ -439,7 +439,7 @@ def events_explore(
"/event_ids",
response_model=list[EventResponse],
dependencies=[Depends(allow_any_authenticated())],
summary="Get events by ids.",
summary="Get events by ids",
description="""Gets events by a list of ids.
Returns a list of events.
""",
@@ -473,7 +473,7 @@ async def event_ids(ids: str, request: Request):
@router.get(
"/events/search",
dependencies=[Depends(allow_any_authenticated())],
summary="Search events.",
summary="Search events",
description="""Searches for events in the database.
Returns a list of events.
""",
@@ -924,7 +924,7 @@ def events_summary(
"/events/{event_id}",
response_model=EventResponse,
dependencies=[Depends(allow_any_authenticated())],
summary="Get event by id.",
summary="Get event by id",
description="Gets an event by its id.",
)
async def event(event_id: str, request: Request):
@@ -968,7 +968,7 @@ def set_retain(event_id: str):
"/events/{event_id}/plus",
response_model=EventUploadPlusResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Send event to Frigate+.",
summary="Send event to Frigate+",
description="""Sends an event to Frigate+.
Returns a success message or an error if the event is not found.
""",
@@ -1207,7 +1207,7 @@ async def false_positive(request: Request, event_id: str):
"/events/{event_id}/retain",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Stop event from being retained indefinitely.",
summary="Stop event from being retained indefinitely",
description="""Stops an event from being retained indefinitely.
Returns a success message or an error if the event is not found.
NOTE: This is a legacy endpoint and is not supported in the frontend.
@@ -1236,7 +1236,7 @@ async def delete_retain(event_id: str, request: Request):
"/events/{event_id}/sub_label",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Set event sub label.",
summary="Set event sub label",
description="""Sets an event's sub label.
Returns a success message or an error if the event is not found.
""",
@@ -1295,7 +1295,7 @@ async def set_sub_label(
"/events/{event_id}/recognized_license_plate",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Set event license plate.",
summary="Set event license plate",
description="""Sets an event's license plate.
Returns a success message or an error if the event is not found.
""",
@@ -1355,7 +1355,7 @@ async def set_plate(
"/events/{event_id}/description",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Set event description.",
summary="Set event description",
description="""Sets an event's description.
Returns a success message or an error if the event is not found.
""",
@@ -1411,7 +1411,7 @@ async def set_description(
"/events/{event_id}/description/regenerate",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Regenerate event description.",
summary="Regenerate event description",
description="""Regenerates an event's description.
Returns a success message or an error if the event is not found.
""",
@@ -1463,8 +1463,8 @@ async def regenerate_description(
@router.post(
"/description/generate",
response_model=GenericResponse,
# dependencies=[Depends(require_role(["admin"]))],
summary="Generate description embedding.",
dependencies=[Depends(require_role(["admin"]))],
summary="Generate description embedding",
description="""Generates an embedding for an event's description.
Returns a success message or an error if the event is not found.
""",
@@ -1529,7 +1529,7 @@ async def delete_single_event(event_id: str, request: Request) -> dict:
"/events/{event_id}",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Delete event.",
summary="Delete event",
description="""Deletes an event from the database.
Returns a success message or an error if the event is not found.
""",
@@ -1544,7 +1544,7 @@ async def delete_event(request: Request, event_id: str):
"/events/",
response_model=EventMultiDeleteResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Delete events.",
summary="Delete events",
description="""Deletes a list of events from the database.
Returns a success message or an error if the events are not found.
""",
@@ -1578,7 +1578,7 @@ async def delete_events(request: Request, body: EventsDeleteBody):
"/events/{camera_name}/{label}/create",
response_model=EventCreateResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Create manual event.",
summary="Create manual event",
description="""Creates a manual event in the database.
Returns a success message or an error if the event is not found.
NOTES:
@@ -1620,7 +1620,7 @@ def create_event(
body.score,
body.sub_label,
body.duration,
body.source_type,
"api",
body.draw,
),
EventMetadataTypeEnum.manual_event_create.value,
@@ -1642,7 +1642,7 @@ def create_event(
"/events/{event_id}/end",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="End manual event.",
summary="End manual event",
description="""Ends a manual event.
Returns a success message or an error if the event is not found.
NOTE: This should only be used for manual events.
@@ -1652,10 +1652,27 @@ async def end_event(request: Request, event_id: str, body: EventsEndBody):
try:
event: Event = Event.get(Event.id == event_id)
await require_camera_access(event.camera, request=request)
if body.end_time is not None and body.end_time < event.start_time:
return JSONResponse(
content=(
{
"success": False,
"message": f"end_time ({body.end_time}) cannot be before start_time ({event.start_time}).",
}
),
status_code=400,
)
end_time = body.end_time or datetime.datetime.now().timestamp()
request.app.event_metadata_updater.publish(
(event_id, end_time), EventMetadataTypeEnum.manual_event_end.value
)
except DoesNotExist:
return JSONResponse(
content=({"success": False, "message": f"Event {event_id} not found."}),
status_code=404,
)
except Exception:
return JSONResponse(
content=(
@@ -1674,7 +1691,7 @@ async def end_event(request: Request, event_id: str, body: EventsEndBody):
"/trigger/embedding",
response_model=dict,
dependencies=[Depends(require_role(["admin"]))],
summary="Create trigger embedding.",
summary="Create trigger embedding",
description="""Creates a trigger embedding for a specific trigger.
Returns a success message or an error if the trigger is not found.
""",
@@ -1832,7 +1849,7 @@ def create_trigger_embedding(
"/trigger/embedding/{camera_name}/{name}",
response_model=dict,
dependencies=[Depends(require_role(["admin"]))],
summary="Update trigger embedding.",
summary="Update trigger embedding",
description="""Updates a trigger embedding for a specific trigger.
Returns a success message or an error if the trigger is not found.
""",
@@ -1997,7 +2014,7 @@ def update_trigger_embedding(
"/trigger/embedding/{camera_name}/{name}",
response_model=dict,
dependencies=[Depends(require_role(["admin"]))],
summary="Delete trigger embedding.",
summary="Delete trigger embedding",
description="""Deletes a trigger embedding for a specific trigger.
Returns a success message or an error if the trigger is not found.
""",
@@ -2071,7 +2088,7 @@ def delete_trigger_embedding(
"/triggers/status/{camera_name}",
response_model=dict,
dependencies=[Depends(require_role(["admin"]))],
summary="Get triggers status.",
summary="Get triggers status",
description="""Gets the status of all triggers for a specific camera.
Returns a success message or an error if the camera is not found.
""",