mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-02 20:00:51 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
540ea1e383 | ||
|
|
47ea942590 | ||
|
|
d5b59f9edd | ||
|
|
91bf509839 | ||
|
|
dd97f48ab6 | ||
|
|
170a992b5a | ||
|
|
2a1d36f2ad | ||
|
|
96ce75cf98 | ||
|
|
0f00cae567 | ||
|
|
63c12cf135 | ||
|
|
e82dc7ad13 | ||
|
|
689a5b59ac | ||
|
|
69061d17f1 | ||
|
|
efb890e6e0 | ||
|
|
ea7b3e2336 |
@@ -181,6 +181,7 @@ noninteractive
|
||||
noprint
|
||||
Norfair
|
||||
nptype
|
||||
NTFS
|
||||
NTSC
|
||||
numpy
|
||||
nvenc
|
||||
|
||||
@@ -42,7 +42,7 @@ runs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -55,6 +55,9 @@ updates:
|
||||
# Needs Python 3.12 and numpy 2
|
||||
- dependency-name: "librosa"
|
||||
versions: [">=1.0"]
|
||||
# pandas 3.1 requires numpy 2
|
||||
- dependency-name: "pandas"
|
||||
versions: [">=3.1"]
|
||||
# Needs Python 3.12
|
||||
- dependency-name: "scipy"
|
||||
versions: [">=1.18"]
|
||||
|
||||
@@ -590,7 +590,7 @@ jobs:
|
||||
with:
|
||||
string: ${{ github.repository }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
string: ${{ github.repository }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ruff == 0.17.1
|
||||
ruff == 0.17.3
|
||||
|
||||
# types
|
||||
types-peewee == 4.0.*
|
||||
|
||||
@@ -2,9 +2,9 @@ aiofiles == 25.1.*
|
||||
click == 8.5.*
|
||||
# FastAPI
|
||||
aiohttp == 3.14.*
|
||||
starlette == 1.9.*
|
||||
starlette == 1.10.*
|
||||
starlette-context == 0.5.*
|
||||
fastapi[standard-no-fastapi-cloud-cli] == 0.159.*
|
||||
fastapi[standard-no-fastapi-cloud-cli] == 0.162.*
|
||||
uvicorn == 0.54.*
|
||||
slowapi == 0.1.*
|
||||
joserfc == 1.7.*
|
||||
@@ -16,7 +16,7 @@ python-multipart == 0.0.32
|
||||
tensorflow == 2.19.* ; platform_machine == 'aarch64'
|
||||
tensorflow-cpu == 2.21.* ; platform_machine == 'x86_64'
|
||||
# General
|
||||
mypy == 2.4.0
|
||||
mypy == 2.4.1
|
||||
onvif-zeep-async == 4.3.*
|
||||
paho-mqtt == 2.1.*
|
||||
pandas == 3.0.*
|
||||
@@ -25,7 +25,7 @@ peewee_migrate == 2.3.*
|
||||
psutil == 7.2.*
|
||||
pydantic == 2.14.*
|
||||
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
||||
tzdata == 2026.5
|
||||
tzdata == 2026.6
|
||||
pyzmq == 27.2.*
|
||||
ruamel.yaml == 0.19.*
|
||||
tzlocal == 5.4.5
|
||||
@@ -45,11 +45,11 @@ scipy == 1.17.*
|
||||
openvino == 2026.4.*
|
||||
onnxruntime == 1.31.*
|
||||
# Embeddings
|
||||
transformers == 5.20.*
|
||||
transformers == 5.22.*
|
||||
# Generative AI
|
||||
google-genai == 2.32.*
|
||||
google-genai == 2.34.*
|
||||
ollama == 0.6.*
|
||||
openai == 3.36.*
|
||||
openai == 3.41.*
|
||||
# push notifications
|
||||
py-vapid == 1.9.6
|
||||
pywebpush == 2.5.*
|
||||
|
||||
@@ -361,7 +361,7 @@ The NPU firmware is loaded by the host kernel and is not part of the Frigate ima
|
||||
|
||||
Frigate bundles a specific version of Intel's [linux-npu-driver](https://github.com/intel/linux-npu-driver/releases), and the host firmware must come from that release or a newer one. Firmware older than the bundled driver may fail with `MAPPED_INFERENCE_VERSION is NOT compatible with the ELF`, where `Expected` is the version the firmware supports and `received` is the version the bundled compiler produced. Distributions often package older firmware than the driver Frigate ships, so check the build date on the host with `sudo dmesg | grep -i vpu` and update it there if needed.
|
||||
|
||||
Intel NPUs cannot be used under Home Assistant OS, which does not include the NPU firmware.
|
||||
Intel NPUs can be used under Home Assistant OS 18.4 or newer.
|
||||
|
||||
### Configuration {#configuration-openvino}
|
||||
|
||||
|
||||
@@ -763,20 +763,17 @@ The community supported docker image tags for the current stable version are:
|
||||
|
||||
:::warning
|
||||
|
||||
As of Home Assistant Operating System 10.2 and Home Assistant 2023.6 defining separate network storage for media is supported.
|
||||
|
||||
There are important limitations in HA OS to be aware of:
|
||||
|
||||
- Separate local storage for media is not yet supported by Home Assistant
|
||||
- AMD GPUs are not supported because HA OS does not include the mesa driver.
|
||||
- Intel NPUs are not supported because HA OS does not include the NPU firmware.
|
||||
- Intel NPUs require Home Assistant OS 18.4 or newer. Earlier releases do not include the NPU firmware.
|
||||
- Nvidia GPUs are not supported because HA Apps do not support the Nvidia runtime.
|
||||
|
||||
:::
|
||||
|
||||
:::tip
|
||||
|
||||
See [the network storage guide](/guides/ha_network_storage.md) for instructions to setup network storage for frigate.
|
||||
Frigate's media can be kept on separate storage, either a network share (Home Assistant 2023.6 or newer) or a local disk such as a second internal drive or a USB drive (Home Assistant 2026.11 or newer). See [the storage guide](/guides/ha_network_storage.md) for setup instructions.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -1,40 +1,48 @@
|
||||
---
|
||||
id: ha_network_storage
|
||||
title: Home Assistant network storage
|
||||
title: Home Assistant storage
|
||||
---
|
||||
|
||||
As of Home Assistant 2023.6, Network Mounted Storage is supported for Apps.
|
||||
Frigate's media (`/media/frigate`) can be stored on a network share or a separate local disk (internal or USB) instead of the Home Assistant system disk.
|
||||
|
||||
## Setting Up Remote Storage For Frigate
|
||||
## Prerequisites
|
||||
|
||||
### Prerequisites
|
||||
### Network storage
|
||||
|
||||
- Home Assistant 2023.6 or newer is installed
|
||||
- Running Home Assistant Operating System 10.2 or newer OR Running Supervised with latest os-agent installed (this is required for supervised install)
|
||||
|
||||
### Local disk
|
||||
|
||||
- Home Assistant 2026.11 or newer is installed
|
||||
- The disk is already formatted, ideally as ext4 (Home Assistant does not format disks). Avoid FAT32, exFAT, and NTFS, which do not support Linux file permissions.
|
||||
|
||||
## Setting Up Storage For Frigate
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. Stop the Frigate App
|
||||
|
||||
### Move current data
|
||||
|
||||
Keeping the current data is optional, but the data will need to be moved regardless so the share can be created successfully.
|
||||
Keeping the current data is optional, but `/media/frigate` must be moved or deleted so Home Assistant can mount the storage there. The database and config are stored elsewhere and are not affected.
|
||||
|
||||
#### If you want to keep the current data
|
||||
|
||||
1. Move the frigate.db, frigate.db-shm, frigate.db-wal files to the /config directory
|
||||
2. Rename the /media/frigate folder to /media/frigate_tmp
|
||||
1. Rename the /media/frigate folder to /media/frigate_tmp
|
||||
|
||||
#### If you don't want to keep the current data
|
||||
|
||||
1. Delete the /media/frigate folder and all of its contents
|
||||
|
||||
### Create the media share
|
||||
### Add the storage
|
||||
|
||||
1. Go to **Settings -> System -> Storage -> Add Network Storage**
|
||||
2. Name the share `frigate` (this is required)
|
||||
3. Choose type `media`
|
||||
4. Fill out the additional required info for your particular NAS
|
||||
1. Go to **Settings -> System -> Storage -> Add storage**
|
||||
2. Name the storage `frigate` (this is required)
|
||||
3. Choose usage `media`
|
||||
4. Choose the type:
|
||||
- For network storage, choose `Samba/Windows (CIFS)` or `Network File System (NFS)` and fill out the additional required info for your particular NAS
|
||||
- For a local disk, choose `Local disk` and select the disk to use
|
||||
5. Connect
|
||||
6. Move files from `/media/frigate_tmp` to `/media/frigate` if they were kept in previous step
|
||||
7. Start the Frigate App
|
||||
|
||||
Vendored
+2
@@ -6371,6 +6371,7 @@ paths:
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: Padding to apply to the vod.
|
||||
default: 0
|
||||
title: Padding
|
||||
@@ -7576,6 +7577,7 @@ paths:
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 30
|
||||
title: Scale
|
||||
responses:
|
||||
|
||||
@@ -966,6 +966,10 @@ def login(request: Request, body: AppPostLoginBody):
|
||||
try:
|
||||
db_user: User = User.get_by_id(user)
|
||||
except DoesNotExist:
|
||||
# Hash anyway so response timing doesn't reveal which usernames exist
|
||||
hash_password(
|
||||
password, iterations=request.app.frigate_config.auth.hash_iterations
|
||||
)
|
||||
logger.warning(f"Login failed for unknown user '{user}' from {remote_addr}")
|
||||
failed_logins.record(user, time.time())
|
||||
return JSONResponse(content={"message": "Login failed"}, status_code=401)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class MediaRecordingsAvailabilityQueryParams(BaseModel):
|
||||
cameras: str = "all"
|
||||
before: float | SkipJsonSchema[None] = None
|
||||
after: float | SkipJsonSchema[None] = None
|
||||
scale: int = 30
|
||||
scale: int = Field(default=30, ge=1)
|
||||
|
||||
|
||||
class RecordingsDeleteQueryParams(BaseModel):
|
||||
|
||||
@@ -291,12 +291,12 @@ def events(
|
||||
clauses.append(
|
||||
reduce(
|
||||
operator.or_,
|
||||
[(start_hour_fun > time_after), (start_hour_fun < time_before)],
|
||||
[(start_hour_fun >= time_after), (start_hour_fun < time_before)],
|
||||
)
|
||||
)
|
||||
# all other cases should be and operator
|
||||
else:
|
||||
clauses.append(start_hour_fun > time_after)
|
||||
clauses.append(start_hour_fun >= time_after)
|
||||
clauses.append(start_hour_fun < time_before)
|
||||
|
||||
if has_clip is not None:
|
||||
@@ -749,12 +749,12 @@ def events_search(
|
||||
event_filters.append(
|
||||
reduce(
|
||||
operator.or_,
|
||||
[(start_hour_fun > time_after), (start_hour_fun < time_before)],
|
||||
[(start_hour_fun >= time_after), (start_hour_fun < time_before)],
|
||||
)
|
||||
)
|
||||
# all other cases should be and operator
|
||||
else:
|
||||
event_filters.append(start_hour_fun > time_after)
|
||||
event_filters.append(start_hour_fun >= time_after)
|
||||
event_filters.append(start_hour_fun < time_before)
|
||||
|
||||
# Perform semantic search
|
||||
|
||||
@@ -1227,7 +1227,7 @@ def bulk_delete_exports(body: ExportBulkDeleteBody):
|
||||
files_in_use = _get_files_in_use()
|
||||
|
||||
for export in exports:
|
||||
if export.video_path.split("/")[-1] in files_in_use:
|
||||
if export.in_progress or export.video_path.split("/")[-1] in files_in_use:
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": False,
|
||||
|
||||
@@ -862,7 +862,7 @@ async def vod_hour(
|
||||
async def vod_event(
|
||||
request: Request,
|
||||
event_id: str,
|
||||
padding: int = Query(0, description="Padding to apply to the vod."),
|
||||
padding: int = Query(0, ge=0, description="Padding to apply to the vod."),
|
||||
):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
@@ -1521,6 +1521,7 @@ async def preview_gif(
|
||||
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
|
||||
)
|
||||
.where(Previews.camera == camera_name)
|
||||
.order_by(Previews.start_time.asc())
|
||||
.limit(1)
|
||||
.get()
|
||||
)
|
||||
@@ -1530,7 +1531,8 @@ async def preview_gif(
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
diff = start_ts - preview.start_time
|
||||
clip_start = max(start_ts, preview.start_time)
|
||||
diff = clip_start - preview.start_time
|
||||
minutes = int(diff / 60)
|
||||
seconds = int(diff % 60)
|
||||
config: FrigateConfig = request.app.frigate_config
|
||||
@@ -1542,7 +1544,7 @@ async def preview_gif(
|
||||
"-ss",
|
||||
f"00:{minutes}:{seconds}",
|
||||
"-t",
|
||||
f"{end_ts - start_ts}",
|
||||
f"{end_ts - clip_start}",
|
||||
"-i",
|
||||
preview.path,
|
||||
"-r",
|
||||
@@ -1709,6 +1711,7 @@ async def preview_mp4(
|
||||
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
|
||||
)
|
||||
.where(Previews.camera == camera_name)
|
||||
.order_by(Previews.start_time.asc())
|
||||
.limit(1)
|
||||
.get()
|
||||
)
|
||||
@@ -1721,7 +1724,8 @@ async def preview_mp4(
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
diff = start_ts - preview.start_time
|
||||
clip_start = max(start_ts, preview.start_time)
|
||||
diff = clip_start - preview.start_time
|
||||
minutes = int(diff / 60)
|
||||
seconds = int(diff % 60)
|
||||
config: FrigateConfig = request.app.frigate_config
|
||||
@@ -1734,7 +1738,7 @@ async def preview_mp4(
|
||||
"-ss",
|
||||
f"00:{minutes}:{seconds}",
|
||||
"-t",
|
||||
f"{end_ts - start_ts}",
|
||||
f"{end_ts - clip_start}",
|
||||
"-i",
|
||||
preview.path,
|
||||
"-r",
|
||||
|
||||
@@ -163,8 +163,7 @@ async def review(
|
||||
|
||||
# Apply ordering and limit
|
||||
review_query = (
|
||||
review_query.order_by(ReviewSegment.severity.asc())
|
||||
.order_by(ReviewSegment.start_time.desc())
|
||||
review_query.order_by(ReviewSegment.start_time.desc())
|
||||
.limit(limit)
|
||||
.dicts()
|
||||
.iterator()
|
||||
|
||||
@@ -12,6 +12,8 @@ import math
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from titlecase import titlecase
|
||||
|
||||
from frigate.models import Event, Timeline
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -84,12 +86,16 @@ def event_name(event: dict[str, Any]) -> str:
|
||||
tracked object whenever a subject is re-detected, so the tracking data
|
||||
cannot say whether two entries are the same subject, and the notes stay
|
||||
ambiguous rather than implying either answer.
|
||||
|
||||
Names are formatted the same way as in the prompt's "Objects in Scene"
|
||||
list. Small models otherwise copy the raw lowercase sub label into their
|
||||
response instead of the name they were told to use.
|
||||
"""
|
||||
label = str(event["label"]).replace("_", " ").replace("-verified", "")
|
||||
sub_label = event.get("sub_label")
|
||||
|
||||
if sub_label:
|
||||
return f'{label} "{sub_label}"'
|
||||
return f'{label} "{titlecase(str(sub_label).replace("_", " "))}"'
|
||||
|
||||
article = "an" if label[:1].lower() in "aeiou" else "a"
|
||||
return f"{article} {label}"
|
||||
@@ -213,6 +219,7 @@ def build_timeline(
|
||||
events: list[dict[str, Any]],
|
||||
span_end: float,
|
||||
state_changes: Sequence[dict[str, Any]] = (),
|
||||
span_start: float | None = None,
|
||||
) -> list[tuple[float, str]]:
|
||||
"""All annotated moments across every event, in time order.
|
||||
|
||||
@@ -222,6 +229,12 @@ def build_timeline(
|
||||
describe nothing the model can see. A track ending means the object
|
||||
stopped being detected, which may or may not mean it left the frame.
|
||||
|
||||
`span_start` is the timestamp of the first sampled frame. An object
|
||||
tracked since before it gets a single "already in view" note instead of
|
||||
its earlier history: those moments would otherwise land on the first
|
||||
frame and read as happening during the clip, turning a car that parked
|
||||
minutes ago into one pulling in.
|
||||
|
||||
`state_changes` are timeline rows (timestamp, source_id, class_type); the
|
||||
stationary and active ones become "has stopped moving" / "starts moving
|
||||
again". Frigate only marks an object stationary after it has been still
|
||||
@@ -249,6 +262,13 @@ def build_timeline(
|
||||
if event["start_time"] > span_end:
|
||||
continue
|
||||
|
||||
if (
|
||||
span_start is not None
|
||||
and event["end_time"]
|
||||
and event["end_time"] < span_start
|
||||
):
|
||||
continue
|
||||
|
||||
points = path_points(event.get("path_data") or [])
|
||||
legs = path_legs(points) if len(points) >= 2 else []
|
||||
name = event_name(event)
|
||||
@@ -259,8 +279,33 @@ def build_timeline(
|
||||
)
|
||||
remaining = list(enumerate(legs))
|
||||
moments: list[tuple[float, str]] = []
|
||||
event_changes = sorted(
|
||||
(timestamp, phrase)
|
||||
for timestamp, phrase in changes_by_event.get(event["id"], [])
|
||||
if timestamp >= detected_at
|
||||
)
|
||||
|
||||
if merges:
|
||||
if span_start is not None and detected_at < span_start:
|
||||
# Only whether it had come to rest is noted; where it is and which
|
||||
# way it is heading are visible in the first frame.
|
||||
earlier = [phrase for t, phrase in event_changes if t < span_start]
|
||||
not_moving = (
|
||||
bool(earlier) and earlier[-1] == STATE_CHANGE_PHRASES["stationary"]
|
||||
)
|
||||
timeline.append(
|
||||
(
|
||||
span_start,
|
||||
f"{name} already in view when the sequence begins"
|
||||
+ (", not moving" if not_moving else ""),
|
||||
)
|
||||
)
|
||||
remaining = [
|
||||
(index, leg)
|
||||
for index, leg in remaining
|
||||
if leg_start_time(points, leg) >= span_start
|
||||
]
|
||||
event_changes = [(t, p) for t, p in event_changes if t >= span_start]
|
||||
elif merges:
|
||||
heading = leg_heading(points, legs[0])
|
||||
timeline.append(
|
||||
(detected_at, f"{name} first detected at {where}, moving {heading}")
|
||||
@@ -277,11 +322,7 @@ def build_timeline(
|
||||
)
|
||||
)
|
||||
|
||||
moments.extend(
|
||||
(timestamp, phrase)
|
||||
for timestamp, phrase in changes_by_event.get(event["id"], [])
|
||||
if timestamp >= detected_at
|
||||
)
|
||||
moments.extend(event_changes)
|
||||
|
||||
for timestamp, phrase in moments:
|
||||
if timestamp <= span_end:
|
||||
@@ -390,7 +431,10 @@ def build_frame_captions(
|
||||
timeline.extend(
|
||||
(timestamp, f"[tracker] {note}")
|
||||
for timestamp, note in build_timeline(
|
||||
events, span_end, get_state_changes(detection_ids)
|
||||
events,
|
||||
span_end,
|
||||
get_state_changes(detection_ids),
|
||||
span_start=frame_times[0],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -386,7 +386,7 @@ class GeminiClient(GenAIClient):
|
||||
|
||||
response = self.provider.models.generate_content(
|
||||
model=self.genai_config.model,
|
||||
contents=gemini_messages, # type: ignore[arg-type]
|
||||
contents=gemini_messages,
|
||||
config=types.GenerateContentConfig(**config_params),
|
||||
)
|
||||
|
||||
@@ -648,7 +648,7 @@ class GeminiClient(GenAIClient):
|
||||
|
||||
stream = await self.provider.aio.models.generate_content_stream(
|
||||
model=self.genai_config.model,
|
||||
contents=gemini_messages, # type: ignore[arg-type]
|
||||
contents=gemini_messages,
|
||||
config=types.GenerateContentConfig(**config_params),
|
||||
)
|
||||
|
||||
|
||||
+20
-11
@@ -18,11 +18,17 @@ from frigate.models import Event
|
||||
|
||||
# Base guidance per response field. `observations` is a reasoning scaffold,
|
||||
# not user-facing, so style presets never override it.
|
||||
#
|
||||
# Every scene and shortSummary guidance states its length. The response
|
||||
# schema caps both fields, and grammar-constrained providers such as llama.cpp
|
||||
# enforce the cap by cutting the text off, so a model that is never told the
|
||||
# limit ends mid-sentence. Every user-facing field also repeats the name rule:
|
||||
# models follow per-field guidance far more reliably than a single global rule.
|
||||
REVIEW_DESCRIPTION_FIELD_GUIDELINES: dict[str, str] = {
|
||||
"observations": "Include the very start of the activity — for example, a vehicle entering the frame or pulling into the driveway — even if it lasts only a few frames and the rest of the clip is dominated by a longer activity. Include each arrival, departure, object handled, and notable change in position or state. Each item is a single concrete fact written as a complete sentence.",
|
||||
"scene": 'Describe how the sequence begins, then the progression of events — all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name — do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign.',
|
||||
"observations": "List what happens in chronological order, starting from the first frame. Include each arrival, departure, object handled, and notable change in position or state, including brief moments at the start or end of the sequence that a longer activity could overshadow. Each item is a single concrete fact written as a complete sentence.",
|
||||
"scene": 'Describe how the sequence begins, then the progression of events: all significant movements and actions in order. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name; do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign. Use no more than five sentences.',
|
||||
"title": "Name the primary activity across the observations, together with the location. An activity is what is being done with objects, tools, or surfaces; locomotion through the scene qualifies as the activity only when no other interaction is observed. For named subjects, always use their name. For unnamed objects, refer to them naturally with articles.",
|
||||
"shortSummary": "Briefly summarize the primary activity across the observations.",
|
||||
"shortSummary": "Briefly summarize the primary activity across the observations in one sentence. For named subjects, always use their name.",
|
||||
"potential_threat_level": "Must be consistent with your scene description and the activity patterns above.",
|
||||
}
|
||||
|
||||
@@ -31,19 +37,19 @@ REVIEW_DESCRIPTION_FIELD_GUIDELINES: dict[str, str] = {
|
||||
# competing style instructions.
|
||||
REVIEW_RESPONSE_STYLES: dict[str, dict[str, str]] = {
|
||||
"natural": {
|
||||
"scene": 'Recount what happened the way a person would describe it to a neighbor, in plain everyday language: how the sequence begins, then each significant movement and action in the order it happens. Use flowing sentences that connect related actions, written the way people actually talk rather than like a surveillance report. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name rather than a generic term. Refer to unnamed objects naturally with articles. Stay factual, and keep the description consistent with the threat level you assign.',
|
||||
"scene": 'Recount what happened the way a person would describe it to a neighbor, in plain everyday language: how the sequence begins, then each significant movement and action in the order it happens. Use flowing sentences that connect related actions, written the way people actually talk rather than like a surveillance report. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name rather than a generic term. Refer to unnamed objects naturally with articles. Stay factual, and keep the description consistent with the threat level you assign. Use no more than five sentences.',
|
||||
"title": 'Write the title as a short, sentence-case headline in present tense: the subject, then what they do, phrased the way you would text it to the homeowner. Describe only the action you see; do not assign the person a role or purpose that is not visibly indicated by a uniform, a marked vehicle, or a "(delivery/service)" tag in Objects in Scene. Name the main thing done, not just movement through the scene, unless movement is all that happens. For named subjects, always use their name.',
|
||||
"shortSummary": "Sum up the primary activity in one short, natural sentence, as if mentioning it to someone in passing.",
|
||||
"shortSummary": "Sum up the primary activity in one short, natural sentence, as if mentioning it to someone in passing. For named subjects, always use their name.",
|
||||
},
|
||||
"concise": {
|
||||
"scene": 'Cover each significant movement and action in order using as few short, direct sentences as possible, omitting environmental and cosmetic detail unless it affects the assessment. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name. For unnamed objects, refer to them naturally with articles. Your description should align with and support the threat level you assign.',
|
||||
"scene": 'Cover each significant movement and action in order using as few short, direct sentences as possible, omitting environmental and cosmetic detail unless it affects the assessment. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name. For unnamed objects, refer to them naturally with articles. Your description should align with and support the threat level you assign. Use no more than five sentences.',
|
||||
"title": 'Write the title as a terse label of two to four words naming the specific activity observed and where it happened. Do not assign a role or purpose that is not visibly indicated by a uniform, a marked vehicle, or a "(delivery/service)" tag in Objects in Scene. For named subjects, always use their name.',
|
||||
"shortSummary": "Summarize the primary activity in one short sentence.",
|
||||
"shortSummary": "Summarize the primary activity in one short sentence. For named subjects, always use their name.",
|
||||
},
|
||||
"detailed": {
|
||||
"scene": 'Describe how the sequence begins, then the progression of events — all significant movements and actions in order, including the specifics that best identify the subjects: colors, clothing, carried items, positions, and paths of movement, plus environmental details like lighting changes when they stand out. Favor the most identifying details over exhaustive coverage, and keep every added detail observational rather than speculative. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name rather than a generic term. For unnamed objects, refer to them naturally with articles. Your description should align with and support the threat level you assign.',
|
||||
"scene": 'Describe how the sequence begins, then the progression of events: all significant movements and actions in order, including the specifics that best identify the subjects: colors, clothing, carried items, positions, and paths of movement, plus environmental details like lighting changes when they stand out. Favor the most identifying details over exhaustive coverage, and keep every added detail observational rather than speculative. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name rather than a generic term. For unnamed objects, refer to them naturally with articles. Your description should align with and support the threat level you assign. Use no more than five sentences.',
|
||||
"title": 'Write the title as a specific description of who did what and where, in under roughly twelve words, including the most distinguishing visible detail of the subject, such as clothing or vehicle color. Do not assign a role or purpose that is not visibly indicated by a uniform, a marked vehicle, or a "(delivery/service)" tag in Objects in Scene. Name the main thing done, not just movement through the scene, unless movement is all that happens. For named subjects, always use their name.',
|
||||
"shortSummary": "Briefly summarize the primary activity across the observations, including the most identifying visible detail, such as vehicle color or clothing.",
|
||||
"shortSummary": "Briefly summarize the primary activity across the observations in one sentence, including the most identifying visible detail, such as vehicle color or clothing. For named subjects, always use their name.",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -61,9 +67,12 @@ def get_review_field_guidelines(response_style: str = "default") -> dict[str, st
|
||||
|
||||
# Explains the per-frame labels and tracker notes used by the annotated frame
|
||||
# mode. Neither the notes nor this guidance say whether repeated detections are
|
||||
# the same subject, since the tracking data cannot tell.
|
||||
# the same subject, since the tracking data cannot tell. The last line keeps
|
||||
# "no longer detected" from being read as the subject leaving the frame, which
|
||||
# is only one of the reasons the tracker loses an object.
|
||||
FRAME_ANNOTATION_GUIDANCE = """- Each image below is immediately preceded by a text label giving its frame number and how many seconds into the sequence it was captured. Use these labels to track the order of events and the time between them.
|
||||
- Some images below are preceded by notes from the camera's object tracker recording what changed at that point: an object being first detected, starting to move, reversing direction, stopping, or no longer being detected. These notes come from tracking data rather than from the images, and they are reliable. Use them to establish how many distinct activities occur and in what order, and describe every one of them."""
|
||||
- Some images below are preceded by notes from the camera's object tracker recording what changed at that point: an object being first detected, starting to move, reversing direction, stopping, or no longer being detected. These notes come from tracking data rather than from the images, and they are reliable. Use them to establish how many distinct activities occur and in what order, and describe every one of them.
|
||||
- The notes say when something changed, not why. In particular, an object is no longer detected whenever the tracker loses sight of it, which happens when it leaves the frame and also when it goes out of view behind or inside something. Look at the frames before and after each note to determine what actually happened."""
|
||||
|
||||
|
||||
def build_review_description_prompt(
|
||||
|
||||
@@ -1470,6 +1470,9 @@ class RecordingExporter(threading.Thread):
|
||||
|
||||
try:
|
||||
self._run_export(video_path, thumb_path)
|
||||
except Exception:
|
||||
self._discard_failed_export(video_path, thumb_path)
|
||||
raise
|
||||
finally:
|
||||
# staged runs hold a full copy of the export, so they must not
|
||||
# survive a failure the way the small chapter file could
|
||||
@@ -1479,7 +1482,8 @@ class RecordingExporter(threading.Thread):
|
||||
"""Drop the partial output and the row that promised it."""
|
||||
Path(video_path).unlink(missing_ok=True)
|
||||
Export.delete().where(Export.id == self.export_id).execute()
|
||||
Path(thumb_path).unlink(missing_ok=True)
|
||||
if thumb_path:
|
||||
Path(thumb_path).unlink(missing_ok=True)
|
||||
|
||||
def _run_export(self, video_path: str, thumb_path: str) -> None:
|
||||
try:
|
||||
@@ -1498,6 +1502,7 @@ class RecordingExporter(threading.Thread):
|
||||
else:
|
||||
ffmpeg_cmd, playlist_lines = self.get_preview_export_command(video_path)
|
||||
except DoesNotExist:
|
||||
self._discard_failed_export(video_path, thumb_path)
|
||||
return
|
||||
|
||||
# When neither custom ffmpeg arg is set the default path uses
|
||||
|
||||
@@ -393,8 +393,8 @@ class TestHttpApp(BaseTestHttp):
|
||||
def test_event_time_filtering(self):
|
||||
morning_id = "123456.random"
|
||||
evening_id = "654321.random"
|
||||
morning = 1656590400 # 06/30/2022 6 am (GMT)
|
||||
evening = 1656633600 # 06/30/2022 6 pm (GMT)
|
||||
morning = 1656568800 # 06/30/2022 6 am (GMT)
|
||||
evening = 1656612000 # 06/30/2022 6 pm (GMT)
|
||||
|
||||
with AuthTestClient(self.app) as client:
|
||||
super().insert_mock_event(morning_id, morning)
|
||||
@@ -417,6 +417,12 @@ class TestHttpApp(BaseTestHttp):
|
||||
).json()
|
||||
assert events
|
||||
assert len(events) == 1
|
||||
# an event in the first minute of the range is included
|
||||
events = client.get(
|
||||
"/events",
|
||||
params={"time_range": "06:00,24:00"},
|
||||
).json()
|
||||
assert len(events) == 2
|
||||
|
||||
def test_set_delete_sub_label(self):
|
||||
mock_event_updater = Mock(spec=EventMetadataPublisher)
|
||||
|
||||
@@ -427,6 +427,30 @@ class TestHttpExport(BaseTestHttp):
|
||||
assert response.status_code == 400
|
||||
assert Export.get(Export.id == "front_door_running").video_path == video
|
||||
|
||||
def test_delete_export_rejected_while_in_progress(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
video = os.path.join(tmpdir, "front_door_abc123.mp4")
|
||||
with open(video, "wb") as handle:
|
||||
handle.write(b"video")
|
||||
|
||||
Export.create(
|
||||
id="front_door_running",
|
||||
camera="front_door",
|
||||
name="front door export",
|
||||
date=100,
|
||||
video_path=video,
|
||||
thumb_path=os.path.join(tmpdir, "t.webp"),
|
||||
in_progress=True,
|
||||
)
|
||||
|
||||
with AuthTestClient(self.app) as client:
|
||||
response = client.post(
|
||||
"/exports/delete", json={"ids": ["front_door_running"]}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert os.path.exists(video)
|
||||
|
||||
def test_rename_export_missing_file_leaves_the_row_alone(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
video = os.path.join(tmpdir, "front_door_gone_abc123.mp4")
|
||||
|
||||
@@ -584,6 +584,10 @@ class TestHttpMedia(BaseTestHttp):
|
||||
{"start_time": 1160, "end_time": 1300},
|
||||
]
|
||||
|
||||
# a non-positive scale would never advance the gap scan
|
||||
response = client.get("/recordings/unavailable", params={"scale": 0})
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_recordings_summary_day_attribution_by_start_time(self):
|
||||
"""
|
||||
A recording spanning midnight marks only its start day.
|
||||
|
||||
@@ -72,6 +72,19 @@ class TestReviewResponseStyle(unittest.TestCase):
|
||||
f"unexpected override fields: {set(overrides)}",
|
||||
)
|
||||
|
||||
def test_every_style_states_length_and_name_rules(self):
|
||||
# The schema caps these fields and grammar-constrained providers cut
|
||||
# text off at the cap, and models only reliably use names when each
|
||||
# field asks for them.
|
||||
for style in ["default", *REVIEW_RESPONSE_STYLES]:
|
||||
fields = get_review_field_guidelines(style)
|
||||
self.assertIn("no more than five sentences", fields["scene"])
|
||||
self.assertIn("one", fields["shortSummary"])
|
||||
self.assertIn("sentence", fields["shortSummary"])
|
||||
|
||||
for field_name in ("scene", "title", "shortSummary"):
|
||||
self.assertIn("always use their name", fields[field_name])
|
||||
|
||||
def test_config_enum_matches_style_presets(self):
|
||||
enum_styles = {e.value for e in ReviewResponseStyleEnum}
|
||||
self.assertEqual(enum_styles, {"default", *REVIEW_RESPONSE_STYLES})
|
||||
|
||||
@@ -82,6 +82,10 @@ class TestEventNames(unittest.TestCase):
|
||||
event = {"label": "waste_bin", "sub_label": "Compost"}
|
||||
self.assertEqual(event_name(event), 'waste bin "Compost"')
|
||||
|
||||
def test_names_match_the_objects_in_scene_list(self):
|
||||
event = {"label": "person", "sub_label": "known_subject"}
|
||||
self.assertEqual(event_name(event), 'person "Known Subject"')
|
||||
|
||||
def test_repeated_objects_are_never_numbered(self):
|
||||
# Whether these are the same subject is unknown, so the notes must not
|
||||
# imply either answer.
|
||||
@@ -297,6 +301,57 @@ class TestLateEvents(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestEarlyEvents(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# A car that pulled in and parked well before the first sampled frame,
|
||||
# then leaves during the clip.
|
||||
self.car = track(
|
||||
"1789481990.000000-parked",
|
||||
"car",
|
||||
0.0,
|
||||
straight_path((0.1, 0.9), (0.4, 0.6), 6, 1.0),
|
||||
)
|
||||
self.changes = [
|
||||
{
|
||||
"timestamp": 12.0,
|
||||
"source_id": self.car["id"],
|
||||
"class_type": "stationary",
|
||||
},
|
||||
{"timestamp": 70.0, "source_id": self.car["id"], "class_type": "active"},
|
||||
]
|
||||
|
||||
def test_history_before_the_first_frame_is_summarized(self):
|
||||
# Arrival notes on the first frame were read as the car pulling in
|
||||
# during the clip.
|
||||
timeline = build_timeline(
|
||||
[self.car], 90.0, state_changes=self.changes, span_start=50.0
|
||||
)
|
||||
self.assertEqual(
|
||||
timeline,
|
||||
[
|
||||
(50.0, "a car already in view when the sequence begins, not moving"),
|
||||
(70.0, "a car starts moving again"),
|
||||
],
|
||||
)
|
||||
|
||||
def test_object_still_moving_at_the_first_frame_is_not_called_stationary(self):
|
||||
timeline = build_timeline([self.car], 90.0, span_start=3.0)
|
||||
self.assertEqual(
|
||||
timeline[0], (3.0, "a car already in view when the sequence begins")
|
||||
)
|
||||
self.assertFalse(any("first detected" in p for _, p in timeline))
|
||||
|
||||
def test_objects_gone_before_the_first_frame_are_skipped(self):
|
||||
gone = dict(self.car, end_time=20.0)
|
||||
self.assertEqual(build_timeline([gone], 90.0, span_start=50.0), [])
|
||||
|
||||
def test_objects_detected_inside_the_clip_are_unchanged(self):
|
||||
self.assertEqual(
|
||||
build_timeline([self.car], 90.0, self.changes, span_start=0.0),
|
||||
build_timeline([self.car], 90.0, self.changes),
|
||||
)
|
||||
|
||||
|
||||
class TestFrameBucketing(unittest.TestCase):
|
||||
def test_moment_attaches_to_the_following_frame(self):
|
||||
frame_times = [0.0, 10.0, 20.0, 30.0]
|
||||
|
||||
@@ -42,6 +42,7 @@ from frigate.config.camera.genai import GenAIConfig, GenAIProviderEnum # noqa:
|
||||
from frigate.genai.prompts import ( # noqa: E402
|
||||
REVIEW_RESPONSE_STYLES,
|
||||
build_review_description_response_format,
|
||||
get_review_field_guidelines,
|
||||
)
|
||||
|
||||
EXAMPLES_DIR = Path(__file__).resolve().parent / "genai-review-examples"
|
||||
@@ -227,11 +228,14 @@ def list_examples() -> list[Path]:
|
||||
)
|
||||
|
||||
|
||||
def load_frames(example: Path) -> list[bytes]:
|
||||
"""Load the example's frames as JPEG bytes in frame order.
|
||||
def load_frames(example: Path) -> tuple[list[bytes], list[str] | None]:
|
||||
"""Load the example's frames as JPEG bytes in frame order, with captions.
|
||||
|
||||
Frames are saved by Frigate as <index>.jpg or <index>.webp. Non-JPEG
|
||||
images are re-encoded to JPEG to match what Frigate sends to providers.
|
||||
In annotated frame mode Frigate also saves <index>.txt, the caption sent
|
||||
directly before that frame; captions are returned only when every frame
|
||||
has one, matching Frigate's fallback to plain frames.
|
||||
"""
|
||||
import cv2
|
||||
|
||||
@@ -242,34 +246,42 @@ def load_frames(example: Path) -> list[bytes]:
|
||||
return (1 << 30, path.name)
|
||||
|
||||
frames: list[bytes] = []
|
||||
captions: list[str] = []
|
||||
files = sorted(
|
||||
(f for f in example.iterdir() if f.suffix.lower() in IMAGE_EXTENSIONS),
|
||||
key=frame_order,
|
||||
)
|
||||
for file in files:
|
||||
if file.suffix.lower() in (".jpg", ".jpeg"):
|
||||
frames.append(file.read_bytes())
|
||||
continue
|
||||
frame = file.read_bytes()
|
||||
else:
|
||||
image = cv2.imread(str(file))
|
||||
if image is None:
|
||||
print(f" Skipping unreadable image {file.name}")
|
||||
continue
|
||||
ok, jpg = cv2.imencode(".jpg", image, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
|
||||
if not ok:
|
||||
continue
|
||||
frame = jpg.tobytes()
|
||||
|
||||
image = cv2.imread(str(file))
|
||||
if image is None:
|
||||
print(f" Skipping unreadable image {file.name}")
|
||||
continue
|
||||
ok, jpg = cv2.imencode(".jpg", image, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
|
||||
if ok:
|
||||
frames.append(jpg.tobytes())
|
||||
return frames
|
||||
frames.append(frame)
|
||||
caption_file = file.with_suffix(".txt")
|
||||
if caption_file.is_file():
|
||||
captions.append(caption_file.read_text())
|
||||
|
||||
return frames, captions if captions and len(captions) == len(frames) else None
|
||||
|
||||
|
||||
def apply_style(prompt: str, style: str) -> str:
|
||||
"""Apply a style preset to a saved prompt.
|
||||
"""Apply the current field guidance for a style preset to a saved prompt.
|
||||
|
||||
Presets replace the per-field response guidance lines, matching what
|
||||
Frigate builds at runtime. Handles both the current guidance format
|
||||
Every per-field response guidance line is replaced with what Frigate
|
||||
builds at runtime for that style, so "default" restores the default
|
||||
guidance on a prompt saved with another style, and older saves pick up
|
||||
current guidance. Handles both the current guidance format
|
||||
("- `scene`: ...") and the 0.17 format ("- `scene` (string): ...").
|
||||
"""
|
||||
overrides = REVIEW_RESPONSE_STYLES.get(style, {})
|
||||
for field_name, guidance in overrides.items():
|
||||
for field_name, guidance in get_review_field_guidelines(style).items():
|
||||
pattern = rf"^- `{field_name}`(?: \(string\))?: .*$"
|
||||
prompt, count = re.subn(
|
||||
pattern,
|
||||
@@ -292,7 +304,9 @@ def pretty_print_response(response: str) -> None:
|
||||
print(json.dumps(parsed, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
def followup_loop(client: Any, base_prompt: str, frames: list[bytes]) -> None:
|
||||
def followup_loop(
|
||||
client: Any, base_prompt: str, frames: list[bytes], captions: list[str] | None
|
||||
) -> None:
|
||||
"""Let the user ask the model follow-up questions about the same frames."""
|
||||
transcript = base_prompt
|
||||
print("\nAsk follow-up questions about this sequence (blank line to finish).")
|
||||
@@ -311,7 +325,7 @@ def followup_loop(client: Any, base_prompt: str, frames: list[bytes]) -> None:
|
||||
f"Question: {question}"
|
||||
)
|
||||
start = time.monotonic()
|
||||
answer = client._send(prompt, frames)
|
||||
answer = client._send(prompt, frames, image_captions=captions)
|
||||
elapsed = time.monotonic() - start
|
||||
if answer is None:
|
||||
print("No response from provider (see logs above).")
|
||||
@@ -349,7 +363,7 @@ def run_example(settings: TesterSettings) -> None:
|
||||
return
|
||||
prompt = apply_style(prompt_file.read_text(), style)
|
||||
|
||||
frames = load_frames(example)
|
||||
frames, captions = load_frames(example)
|
||||
if not frames:
|
||||
print(f"{example.name} contains no frame images, cannot run")
|
||||
return
|
||||
@@ -365,10 +379,11 @@ def run_example(settings: TesterSettings) -> None:
|
||||
|
||||
print(
|
||||
f"\nSending {len(frames)} frames to {settings.provider} "
|
||||
f"(model={settings.model or '(none)'}, style={style})..."
|
||||
f"(model={settings.model or '(none)'}, style={style}, "
|
||||
f"captions={'yes' if captions else 'no'})..."
|
||||
)
|
||||
start = time.monotonic()
|
||||
response = client._send(prompt, frames, response_format)
|
||||
response = client._send(prompt, frames, response_format, image_captions=captions)
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
if response is None:
|
||||
@@ -377,7 +392,7 @@ def run_example(settings: TesterSettings) -> None:
|
||||
|
||||
print(f"\nResponse ({elapsed:.1f}s):\n")
|
||||
pretty_print_response(response)
|
||||
followup_loop(client, f"{prompt}\n\nYour analysis:\n{response}", frames)
|
||||
followup_loop(client, f"{prompt}\n\nYour analysis:\n{response}", frames, captions)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -97,12 +97,13 @@ test.describe("setup wizard account @high @mobile", () => {
|
||||
.fill("a-long-enough-password");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
|
||||
const createCall = sent.find((call) => call.method === "POST");
|
||||
expect(createCall?.body).toEqual({
|
||||
username: "family",
|
||||
password: "a-long-enough-password",
|
||||
role: "viewer",
|
||||
});
|
||||
await expect
|
||||
.poll(() => sent.find((call) => call.method === "POST")?.body)
|
||||
.toEqual({
|
||||
username: "family",
|
||||
password: "a-long-enough-password",
|
||||
role: "viewer",
|
||||
});
|
||||
});
|
||||
|
||||
test("shows anonymous copy when nobody is signed in", async ({
|
||||
|
||||
@@ -101,10 +101,14 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
await expect(page.getByText("Will use VAAPI (Intel/AMD)")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
|
||||
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({
|
||||
hwaccel_args: "preset-vaapi",
|
||||
});
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
saves.find((save) => save.config_data?.ffmpeg)?.config_data?.ffmpeg,
|
||||
)
|
||||
.toEqual({
|
||||
hwaccel_args: "preset-vaapi",
|
||||
});
|
||||
|
||||
// the saved steps only take effect after a restart
|
||||
const restarts = await captureRestarts(page);
|
||||
@@ -117,7 +121,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
await page.getByRole("button", { name: "Apply & Restart" }).click();
|
||||
|
||||
await expect(page.getByText("Starting Frigate...")).toBeVisible();
|
||||
expect(restarts).toHaveLength(1);
|
||||
await expect.poll(() => restarts).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("defers model setup for onnx hardware without Frigate+", async ({
|
||||
@@ -228,10 +232,14 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// no camera was added, so there is no codec to match
|
||||
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
|
||||
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({
|
||||
hwaccel_args: "preset-jetson-h264",
|
||||
});
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
saves.find((save) => save.config_data?.ffmpeg)?.config_data?.ffmpeg,
|
||||
)
|
||||
.toEqual({
|
||||
hwaccel_args: "preset-jetson-h264",
|
||||
});
|
||||
});
|
||||
|
||||
test("None writes an explicit empty hwaccel list", async ({
|
||||
@@ -249,7 +257,11 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
await page.getByRole("radio", { name: "None (software decoding)" }).click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
|
||||
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({ hwaccel_args: [] });
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
saves.find((save) => save.config_data?.ffmpeg)?.config_data?.ffmpeg,
|
||||
)
|
||||
.toEqual({ hwaccel_args: [] });
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+164
-164
@@ -9,8 +9,8 @@
|
||||
"version": "0.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@cycjimmy/jsmpeg-player": "^6.1.2",
|
||||
"@hookform/resolvers": "^6.0.0",
|
||||
"@cycjimmy/jsmpeg-player": "^6.1.8",
|
||||
"@hookform/resolvers": "^6.0.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.2.1",
|
||||
"@radix-ui/react-aspect-ratio": "^1.2.0",
|
||||
"@radix-ui/react-checkbox": "^1.4.1",
|
||||
@@ -37,39 +37,39 @@
|
||||
"@rjsf/shadcn": "^6.10.1",
|
||||
"@rjsf/utils": "^6.10.1",
|
||||
"@rjsf/validator-ajv8": "^6.10.1",
|
||||
"apexcharts": "^8.4.1",
|
||||
"axios": "^1.20.0",
|
||||
"apexcharts": "^8.6.0",
|
||||
"axios": "^1.21.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"copy-to-clipboard": "^4.0.2",
|
||||
"date-fns": "^4.4.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"framer-motion": "^14.6.0",
|
||||
"hls.js": "^1.7.3",
|
||||
"i18next": "^26.5.5",
|
||||
"i18next-http-backend": "^4.0.4",
|
||||
"framer-motion": "^14.9.0",
|
||||
"hls.js": "^1.7.4",
|
||||
"i18next": "^26.5.12",
|
||||
"i18next-http-backend": "^4.0.7",
|
||||
"idb-keyval": "^6.3.0",
|
||||
"js-yaml": "^5.5.0",
|
||||
"konva": "^10.9.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lucide-react": "^1.61.0",
|
||||
"lucide-react": "^1.65.0",
|
||||
"monaco-yaml": "^5.5.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"react": "^19.3.1",
|
||||
"react-apexcharts": "^2.1.1",
|
||||
"react-apexcharts": "^2.2.0",
|
||||
"react-day-picker": "^10.0.2",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^19.3.1",
|
||||
"react-dropzone": "^20.1.4",
|
||||
"react-grid-layout": "^2.3.0",
|
||||
"react-hook-form": "^7.90.0",
|
||||
"react-i18next": "^17.1.1",
|
||||
"react-hook-form": "^7.91.0",
|
||||
"react-i18next": "^17.1.9",
|
||||
"react-icons": "^5.7.0",
|
||||
"react-konva": "^19.3.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.18.4",
|
||||
"react-router-dom": "^7.18.5",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"react-zoom-pan-pinch": "3.4.4",
|
||||
"remark-gfm": "^4.0.0",
|
||||
@@ -81,7 +81,7 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-long-press": "^3.3.0",
|
||||
"vaul": "^1.1.2",
|
||||
"virtua": "^0.53.6",
|
||||
"virtua": "^0.54.0",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"zod": "^4.6.7"
|
||||
},
|
||||
@@ -90,13 +90,13 @@
|
||||
"@playwright/test": "^1.64.1",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.27",
|
||||
"@types/node": "^26.6.5",
|
||||
"@types/node": "^26.6.6",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react": "^6.1.2",
|
||||
"autoprefixer": "^10.6.1",
|
||||
"autoprefixer": "^10.6.2",
|
||||
"esbuild": "^0.28.2",
|
||||
"eslint": "^10.13.0",
|
||||
"eslint": "^10.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.7",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
@@ -105,12 +105,12 @@
|
||||
"i18next-cli": "^1.5.11",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"postcss": "^8.5.31",
|
||||
"postcss": "^8.5.32",
|
||||
"prettier": "^3.9.10",
|
||||
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.72.0",
|
||||
"typescript-eslint": "^8.72.1",
|
||||
"vite": "^8.3.5"
|
||||
}
|
||||
},
|
||||
@@ -428,9 +428,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@cycjimmy/jsmpeg-player": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.1.2.tgz",
|
||||
"integrity": "sha512-U9DBDe5fxHmbwQww9rFxMLNI2Wlg7DhPzI7AVFpq8GehiUP7+NwuMPXpP4zAd52sgkxtOqOeMjgE5g0ZLnQZ0w==",
|
||||
"version": "6.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.1.8.tgz",
|
||||
"integrity": "sha512-ccG7JvidQWWPvmmpu6Ob6TUnbyaz6WaTHl9V/5BD4vBVndxnxchLmVU3rpJguB2lWEoEAhFPBIU3GoeXtc3RaQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@date-fns/tz": {
|
||||
@@ -1059,9 +1059,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-6.0.0.tgz",
|
||||
"integrity": "sha512-6iBzQnB+YwAveVc8HUi6QYLITn5X+xNKRMFIcNXHZlvp2aTI7yWbj4n/JodXxirMCcBc4VCjb2aYAJh4x07mVw==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-6.0.2.tgz",
|
||||
"integrity": "sha512-e2dqUOlaGuirKVOd/iZWk/Yg5zeaGw7t0hSc3LzxL3o7eZGwZkTyEbBL8H52m0wRKEsNLWoMGOiXGHbPHTXlEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/utils": "^0.3.0"
|
||||
@@ -1069,7 +1069,6 @@
|
||||
"peerDependencies": {
|
||||
"@sinclair/typebox": ">=0.25.24",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@typeschema/main": ">=0.13.7",
|
||||
"@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-errors": "^3.0.0",
|
||||
@@ -1100,9 +1099,6 @@
|
||||
"@standard-schema/spec": {
|
||||
"optional": true
|
||||
},
|
||||
"@typeschema/main": {
|
||||
"optional": true
|
||||
},
|
||||
"@vinejs/vine": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -3611,9 +3607,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
|
||||
"integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
|
||||
"version": "26.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
|
||||
"integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3661,17 +3657,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz",
|
||||
"integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz",
|
||||
"integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.72.0",
|
||||
"@typescript-eslint/type-utils": "8.72.0",
|
||||
"@typescript-eslint/utils": "8.72.0",
|
||||
"@typescript-eslint/visitor-keys": "8.72.0",
|
||||
"@typescript-eslint/scope-manager": "8.72.1",
|
||||
"@typescript-eslint/type-utils": "8.72.1",
|
||||
"@typescript-eslint/utils": "8.72.1",
|
||||
"@typescript-eslint/visitor-keys": "8.72.1",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -3684,15 +3680,15 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.72.0",
|
||||
"@typescript-eslint/parser": "^8.72.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
"version": "7.0.13",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
|
||||
"integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
|
||||
"version": "7.0.14",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
|
||||
"integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3700,16 +3696,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz",
|
||||
"integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz",
|
||||
"integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.72.0",
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/typescript-estree": "8.72.0",
|
||||
"@typescript-eslint/visitor-keys": "8.72.0",
|
||||
"@typescript-eslint/scope-manager": "8.72.1",
|
||||
"@typescript-eslint/types": "8.72.1",
|
||||
"@typescript-eslint/typescript-estree": "8.72.1",
|
||||
"@typescript-eslint/visitor-keys": "8.72.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3725,14 +3721,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz",
|
||||
"integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz",
|
||||
"integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.72.0",
|
||||
"@typescript-eslint/types": "^8.72.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.72.1",
|
||||
"@typescript-eslint/types": "^8.72.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3747,14 +3743,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz",
|
||||
"integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz",
|
||||
"integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/visitor-keys": "8.72.0"
|
||||
"@typescript-eslint/types": "8.72.1",
|
||||
"@typescript-eslint/visitor-keys": "8.72.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3765,9 +3761,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz",
|
||||
"integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz",
|
||||
"integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3782,15 +3778,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz",
|
||||
"integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz",
|
||||
"integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/typescript-estree": "8.72.0",
|
||||
"@typescript-eslint/utils": "8.72.0",
|
||||
"@typescript-eslint/types": "8.72.1",
|
||||
"@typescript-eslint/typescript-estree": "8.72.1",
|
||||
"@typescript-eslint/utils": "8.72.1",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -3807,9 +3803,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz",
|
||||
"integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz",
|
||||
"integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3821,16 +3817,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz",
|
||||
"integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz",
|
||||
"integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.72.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.72.0",
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/visitor-keys": "8.72.0",
|
||||
"@typescript-eslint/project-service": "8.72.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.72.1",
|
||||
"@typescript-eslint/types": "8.72.1",
|
||||
"@typescript-eslint/visitor-keys": "8.72.1",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -3849,16 +3845,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz",
|
||||
"integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz",
|
||||
"integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.72.0",
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/typescript-estree": "8.72.0"
|
||||
"@typescript-eslint/scope-manager": "8.72.1",
|
||||
"@typescript-eslint/types": "8.72.1",
|
||||
"@typescript-eslint/typescript-estree": "8.72.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3873,13 +3869,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz",
|
||||
"integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz",
|
||||
"integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.72.0",
|
||||
"@typescript-eslint/types": "8.72.1",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4067,9 +4063,9 @@
|
||||
"license": "SEE LICENSE IN LICENSE.md"
|
||||
},
|
||||
"node_modules/apexcharts": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-8.4.1.tgz",
|
||||
"integrity": "sha512-29IbZ1rn3NRkDvete5EzZXy6yDSW7mjNXkytLpvaYHk0XbfsR+FvC7U78L3wMvJa699NBmix2z6r3Qm5YDvw0A==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-8.6.0.tgz",
|
||||
"integrity": "sha512-5524MLoDei2n8EcVMK+u3eOZTWrTpT1BIPRWu4AK8kkJdkODWs1hp3IEax8pMz78sSNAtsD4Tp7tr0s0SW/npQ==",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"dependencies": {
|
||||
"apex-commons": "^0.8.3"
|
||||
@@ -4126,9 +4122,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.6.1",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz",
|
||||
"integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==",
|
||||
"version": "10.6.2",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.2.tgz",
|
||||
"integrity": "sha512-VOw9/yGTGwPPgzFcODtcv6zhEPIeIWkRSe10L2ri2f5vZ7ID7lCt6jZzRF5/ubgOZ+szYydmEF5AYsA5jxX1tA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -4163,9 +4159,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
|
||||
"integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.21.0.tgz",
|
||||
"integrity": "sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
@@ -4972,9 +4968,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "10.13.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz",
|
||||
"integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==",
|
||||
"version": "10.14.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz",
|
||||
"integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -4987,7 +4983,7 @@
|
||||
"@eslint/config-helpers": "^0.7.0",
|
||||
"@eslint/core": "^1.2.1",
|
||||
"@eslint/plugin-kit": "^0.7.3",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanfs/node": "^0.16.8",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
@@ -5008,7 +5004,7 @@
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"minimatch": "^10.2.5",
|
||||
"minimatch": "^10.2.6",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
@@ -5515,9 +5511,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
|
||||
"integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz",
|
||||
"integrity": "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -5582,18 +5578,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "14.6.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.6.0.tgz",
|
||||
"integrity": "sha512-MzBgZxGgoHPfSLEXmo4o5XlZvb+nVRFdHtGjVhm6LMIEJKTt/8hNO023euL5mYyzc8llV/hQTfjnN68tNRHalw==",
|
||||
"version": "14.9.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.9.0.tgz",
|
||||
"integrity": "sha512-g1hZu8WoTwgsoVAaqEEcefhvR03cKGtkmrKaRrWpXhnM3LGR8jl1u2qyJ1PMVlO7LKQHE8TTRQYUlMmnrY4L+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "14.6.0",
|
||||
"motion-utils": "14.6.0",
|
||||
"motion-dom": "14.9.0",
|
||||
"motion-utils": "14.7.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
"react-dom": "^18.0.0 || ^19.0.0",
|
||||
"remix": "^3.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@@ -5601,6 +5598,9 @@
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"remix": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5949,9 +5949,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.3.tgz",
|
||||
"integrity": "sha512-MsPlx6yVW4Qv4C7mEVou4/gk/5cN2dVxOTnrNjPSmyH2f0Ln+/UkG10Ax1PE6OvrmTVPKgQYozTtBaPOuCzq7A==",
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.4.tgz",
|
||||
"integrity": "sha512-3GlxGdPvT2YHvQYOt+yOzT/DEior/qJVNIylHA77kgeoBTdQQqi70GwefNEUmHQeNGyjC8U62+bruqW/Tpm+tg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/hookified": {
|
||||
@@ -5991,9 +5991,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.5.5",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.5.tgz",
|
||||
"integrity": "sha512-txdaNOlnZCdiTMSlEzl5hUJU8TC9R4J/RCR1G7XrDPbWT2t3mR142QPz84ZuZeXDt3yq+9zmsdPiakxYtmp3fw==",
|
||||
"version": "26.5.12",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.12.tgz",
|
||||
"integrity": "sha512-vTt8tc3y5axoU7wwTYg8vjpILLyP33a5GyGJAVI7FDRx1XueYxZVIkcVeStoftrVnAM/o28hKvK0sslucDI9aQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -6106,9 +6106,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-http-backend": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.4.tgz",
|
||||
"integrity": "sha512-nG9OQKvUsPiTVUBg1rCGmvmWhzxC9GOxVbGdTYyejHVMSUjutt1hiBNVSzhB/ZE7keL9Xkb880kUfhDovV5T5g==",
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.7.tgz",
|
||||
"integrity": "sha512-SQy3OPy66YzumNJ43boHDBtzzMISnoPhQ0Zqjetk+luW74Tok8Ez9ujJJbJHKSHkG5sjDGldHznjuPtSxBwvuw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -6978,9 +6978,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.61.0.tgz",
|
||||
"integrity": "sha512-v2SQPPPNjnr+BlhL9Spuzmo5yR+hLkftfuE4E3jaxdNPgSH0hJKPcLJBP7yAta87jQNPsSERS59PJjCJ/CorBA==",
|
||||
"version": "1.65.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.65.0.tgz",
|
||||
"integrity": "sha512-roSdtzudEY39i2hn5LLjA8toYfhF2z4S10WSqZF4ubXpHlOmNTvaxMxZLdJfQDi6pEifQWX+cRmYihKSsvAxfA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -8055,18 +8055,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "14.6.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.6.0.tgz",
|
||||
"integrity": "sha512-lIojTl9aYSPIkeOOSzBliy1nNvpdO3JtVzW2WgTXQlOP4ZMuJhtWO3fP7OD4TVFwlRucdvxlDSMkL+fEQnQ52A==",
|
||||
"version": "14.9.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.9.0.tgz",
|
||||
"integrity": "sha512-wGjKbNDMHd75aLStkxLCKZ+T8/wusZHklmNLK1Ak68UrdBt2X8I4yweYR/0WonxGnBsw6oloDTIV8pN+UTvzzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "14.6.0"
|
||||
"motion-utils": "14.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "14.6.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.6.0.tgz",
|
||||
"integrity": "sha512-bZJo2AmQ2aOFkaato6VXrTbkPG0XhMzivWUWSaI59pxyllj8QMQUFZ84MYRQnPkK+7dr4KldyJwF2+RM0sdq/w==",
|
||||
"version": "14.7.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.7.0.tgz",
|
||||
"integrity": "sha512-woO0/Nb7ys75f94fjvQi/6leiHGvZUYkeHUPEZ3vxsV+9OpEVhkxp01x8MlRwX0zt31EqgXsKDAoBoJLC148Tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
@@ -8548,9 +8548,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
|
||||
"integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
|
||||
"version": "8.5.32",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
|
||||
"integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -8920,10 +8920,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-apexcharts": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-2.1.1.tgz",
|
||||
"integrity": "sha512-aV25n+1ZdAzRG0KhODVC48K8WbhMKBAr3yWSeaaBqM1ySn22kSSKblsNCUvlwegsT+cdMLuOrK9xGZb+IK+OkQ==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-2.2.0.tgz",
|
||||
"integrity": "sha512-MEiH/T65ooUNf8V7Cq7fCu1femP3+lN9Svzsv+jNNS6AcRSciyA1LLvc1VETWqAiqMxQyX8NYEhZxOYnMf262g==",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
@@ -9037,9 +9037,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.90.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.90.0.tgz",
|
||||
"integrity": "sha512-+NdG0FLzCPhFOp2NNxaUYLF9fyZhNjx9FTBBSYWF71/l6BBlmIc/j6xlp3DbaHWLJbIG7Zd8oznA19xJClU97w==",
|
||||
"version": "7.91.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.91.0.tgz",
|
||||
"integrity": "sha512-5BEBUnCBwa28AFXYTYmOaB/+CSpJ/+CWVASHEvIGUzFdHyUiPHeRhG7r5hOZ8nZi0yP7pbHdFNXO/p31EKTJFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -9059,9 +9059,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "17.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.1.1.tgz",
|
||||
"integrity": "sha512-Q76xpzxrZNcqhB7M31Ns7isulGOlmhURqRsJOGlYgJJCyoy1pMq6tRl8KQycGNNY1vPaxbIdrogrqY9nkb+SEA==",
|
||||
"version": "17.1.9",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.1.9.tgz",
|
||||
"integrity": "sha512-OgYqIH13XHPlWTaaI795Dz+k2AdKnXcOxNQVnf750T8Ij0TYWp1nYvXRpoJBulPqlyk2OAbn1PM4tFKiIj/byA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.7",
|
||||
@@ -9235,9 +9235,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.18.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz",
|
||||
"integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==",
|
||||
"version": "7.18.5",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.5.tgz",
|
||||
"integrity": "sha512-dTgvcXDOrLPrlLHrcZAqWveiK1hMSqN+rGAJzegP8HtmNkfRxOoshGWBNX3ZJ0cZjOHkRdlUJtaIhZMLLyGyPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
@@ -9257,12 +9257,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.18.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz",
|
||||
"integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==",
|
||||
"version": "7.18.5",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.5.tgz",
|
||||
"integrity": "sha512-9HcfDMU6JJ/fp6gJZYGwNE6RknVGS4kBjH1HkNw959mCJi70TVwdipQWv2FEol9uGpi+SZTFtVpJPtEQfayeOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.18.4"
|
||||
"react-router": "7.18.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -9586,9 +9586,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.3.tgz",
|
||||
"integrity": "sha512-irZHtTqurLna57KQG/s+fzOwv/J6XSH4cs8M/vsrY3uCaQ3S6+QJguAH0WdN47SXlk82voMZ4naWMQf8fdEoIg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
@@ -10062,16 +10062,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.72.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.0.tgz",
|
||||
"integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==",
|
||||
"version": "8.72.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz",
|
||||
"integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.72.0",
|
||||
"@typescript-eslint/parser": "8.72.0",
|
||||
"@typescript-eslint/typescript-estree": "8.72.0",
|
||||
"@typescript-eslint/utils": "8.72.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.72.1",
|
||||
"@typescript-eslint/parser": "8.72.1",
|
||||
"@typescript-eslint/typescript-estree": "8.72.1",
|
||||
"@typescript-eslint/utils": "8.72.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -10364,9 +10364,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/virtua": {
|
||||
"version": "0.53.6",
|
||||
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.53.6.tgz",
|
||||
"integrity": "sha512-/vQtSFNDFcp3Yz4klbpe8Z88eQeThSw49cFlgJQdsqh3SUnRlW4jLi6ZYt4YzdwerYGlP6KCgAOXNVO+PmUg7w==",
|
||||
"version": "0.54.0",
|
||||
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.54.0.tgz",
|
||||
"integrity": "sha512-8nGGrDhX61MDVeqgyIaWrJqhRm9z2Nqa1WDqt0tk1m+ZWrvLesMP0iL0vL6xNsLCF2jE2ohwY+PqufdbVmRgEw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=20.0.0",
|
||||
|
||||
+19
-19
@@ -21,8 +21,8 @@
|
||||
"i18n:status": "i18next-cli status"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cycjimmy/jsmpeg-player": "^6.1.2",
|
||||
"@hookform/resolvers": "^6.0.0",
|
||||
"@cycjimmy/jsmpeg-player": "^6.1.8",
|
||||
"@hookform/resolvers": "^6.0.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.2.1",
|
||||
"@radix-ui/react-aspect-ratio": "^1.2.0",
|
||||
"@radix-ui/react-checkbox": "^1.4.1",
|
||||
@@ -49,39 +49,39 @@
|
||||
"@rjsf/shadcn": "^6.10.1",
|
||||
"@rjsf/utils": "^6.10.1",
|
||||
"@rjsf/validator-ajv8": "^6.10.1",
|
||||
"apexcharts": "^8.4.1",
|
||||
"axios": "^1.20.0",
|
||||
"apexcharts": "^8.6.0",
|
||||
"axios": "^1.21.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"copy-to-clipboard": "^4.0.2",
|
||||
"date-fns": "^4.4.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"framer-motion": "^14.6.0",
|
||||
"hls.js": "^1.7.3",
|
||||
"i18next": "^26.5.5",
|
||||
"i18next-http-backend": "^4.0.4",
|
||||
"framer-motion": "^14.9.0",
|
||||
"hls.js": "^1.7.4",
|
||||
"i18next": "^26.5.12",
|
||||
"i18next-http-backend": "^4.0.7",
|
||||
"idb-keyval": "^6.3.0",
|
||||
"js-yaml": "^5.5.0",
|
||||
"konva": "^10.9.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lucide-react": "^1.61.0",
|
||||
"lucide-react": "^1.65.0",
|
||||
"monaco-yaml": "^5.5.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"react": "^19.3.1",
|
||||
"react-apexcharts": "^2.1.1",
|
||||
"react-apexcharts": "^2.2.0",
|
||||
"react-day-picker": "^10.0.2",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^19.3.1",
|
||||
"react-dropzone": "^20.1.4",
|
||||
"react-grid-layout": "^2.3.0",
|
||||
"react-hook-form": "^7.90.0",
|
||||
"react-i18next": "^17.1.1",
|
||||
"react-hook-form": "^7.91.0",
|
||||
"react-i18next": "^17.1.9",
|
||||
"react-icons": "^5.7.0",
|
||||
"react-konva": "^19.3.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.18.4",
|
||||
"react-router-dom": "^7.18.5",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"react-zoom-pan-pinch": "3.4.4",
|
||||
"remark-gfm": "^4.0.0",
|
||||
@@ -93,7 +93,7 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-long-press": "^3.3.0",
|
||||
"vaul": "^1.1.2",
|
||||
"virtua": "^0.53.6",
|
||||
"virtua": "^0.54.0",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"zod": "^4.6.7"
|
||||
},
|
||||
@@ -102,13 +102,13 @@
|
||||
"@playwright/test": "^1.64.1",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.27",
|
||||
"@types/node": "^26.6.5",
|
||||
"@types/node": "^26.6.6",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react": "^6.1.2",
|
||||
"autoprefixer": "^10.6.1",
|
||||
"autoprefixer": "^10.6.2",
|
||||
"esbuild": "^0.28.2",
|
||||
"eslint": "^10.13.0",
|
||||
"eslint": "^10.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.7",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
@@ -117,12 +117,12 @@
|
||||
"i18next-cli": "^1.5.11",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"postcss": "^8.5.31",
|
||||
"postcss": "^8.5.32",
|
||||
"prettier": "^3.9.10",
|
||||
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.72.0",
|
||||
"typescript-eslint": "^8.72.1",
|
||||
"vite": "^8.3.5"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -1086,13 +1086,6 @@
|
||||
"title": "Confirm Password",
|
||||
"placeholder": "Confirm Password"
|
||||
},
|
||||
"strength": {
|
||||
"title": "Password strength: ",
|
||||
"weak": "Weak",
|
||||
"medium": "Medium",
|
||||
"strong": "Strong",
|
||||
"veryStrong": "Very Strong"
|
||||
},
|
||||
"requirements": {
|
||||
"title": "Password requirements:",
|
||||
"length": "At least 12 characters"
|
||||
|
||||
@@ -415,10 +415,19 @@ export default function NotificationsSettingsExtras({
|
||||
pushSubscription.unsubscribe();
|
||||
workerRegistration.unregister();
|
||||
setRegistration(null);
|
||||
removeMessage("notification_settings", "registration");
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(t("notification.toast.error.registerFailed"), {
|
||||
position: "top-center",
|
||||
});
|
||||
workerRegistration.unregister();
|
||||
setRegistration(null);
|
||||
removeMessage("notification_settings", "registration");
|
||||
});
|
||||
},
|
||||
[addMessage, publicKey, t],
|
||||
[addMessage, removeMessage, publicKey, t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -307,6 +307,9 @@ export function KnownPlatesField(props: FieldProps) {
|
||||
);
|
||||
|
||||
const handleAddEntry = useCallback(() => {
|
||||
if ("" in data) {
|
||||
return;
|
||||
}
|
||||
const next = { ...data, "": [""] };
|
||||
onChange(next, fieldPath);
|
||||
}, [data, fieldPath, onChange]);
|
||||
|
||||
@@ -37,12 +37,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { isDesktop, isMobile } from "react-device-detect";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import {
|
||||
calculatePasswordStrength,
|
||||
getPasswordRequirements,
|
||||
getPasswordStrengthLabel,
|
||||
getPasswordStrengthColor,
|
||||
} from "@/utils/passwordUtil";
|
||||
import { getPasswordRequirements } from "@/utils/passwordUtil";
|
||||
import {
|
||||
MobilePage,
|
||||
MobilePageContent,
|
||||
@@ -123,12 +118,6 @@ export default function CreateUserDialog({
|
||||
const passwordsMatch = password === confirmPassword;
|
||||
const showMatchIndicator = password && confirmPassword;
|
||||
|
||||
// Password strength calculation
|
||||
const passwordStrength = useMemo(
|
||||
() => calculatePasswordStrength(password),
|
||||
[password],
|
||||
);
|
||||
|
||||
const requirements = useMemo(
|
||||
() => getPasswordRequirements(password),
|
||||
[password],
|
||||
@@ -224,6 +213,12 @@ export default function CreateUserDialog({
|
||||
type={showPasswordVisible ? "text" : "password"}
|
||||
className="h-10 pr-10"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
if (form.getValues("confirmPassword")) {
|
||||
form.trigger("confirmPassword");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -255,21 +250,6 @@ export default function CreateUserDialog({
|
||||
|
||||
{password && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-secondary-foreground">
|
||||
<div
|
||||
className={`${getPasswordStrengthColor(
|
||||
password,
|
||||
)} transition-all duration-300`}
|
||||
style={{ width: `${passwordStrength * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("users.dialog.form.password.strength.title")}
|
||||
<span className="font-medium">
|
||||
{getPasswordStrengthLabel(password, t)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="space-y-1 rounded-md bg-muted/50 p-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
{t("users.dialog.form.password.requirements.title")}
|
||||
@@ -297,8 +277,6 @@ export default function CreateUserDialog({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -366,7 +344,6 @@ export default function CreateUserDialog({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -690,36 +690,8 @@ export function ExportContent({
|
||||
(option: ExportOption) => {
|
||||
setSelectedOption(option);
|
||||
|
||||
const now = new Date(latestTime * 1000);
|
||||
let start: number;
|
||||
|
||||
switch (option) {
|
||||
case "1":
|
||||
now.setHours(now.getHours() - 1);
|
||||
start = now.getTime() / 1000;
|
||||
break;
|
||||
case "4":
|
||||
now.setHours(now.getHours() - 4);
|
||||
start = now.getTime() / 1000;
|
||||
break;
|
||||
case "8":
|
||||
now.setHours(now.getHours() - 8);
|
||||
start = now.getTime() / 1000;
|
||||
break;
|
||||
case "12":
|
||||
now.setHours(now.getHours() - 12);
|
||||
start = now.getTime() / 1000;
|
||||
break;
|
||||
case "24":
|
||||
now.setHours(now.getHours() - 24);
|
||||
start = now.getTime() / 1000;
|
||||
break;
|
||||
case "custom":
|
||||
start = latestTime - 3600;
|
||||
break;
|
||||
default:
|
||||
start = latestTime - 3600;
|
||||
}
|
||||
const hours = Number(option);
|
||||
const start = latestTime - (Number.isNaN(hours) ? 1 : hours) * 3600;
|
||||
|
||||
setRange({
|
||||
before: latestTime,
|
||||
|
||||
@@ -28,12 +28,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import {
|
||||
calculatePasswordStrength,
|
||||
getPasswordRequirements,
|
||||
getPasswordStrengthLabel,
|
||||
getPasswordStrengthColor,
|
||||
} from "@/utils/passwordUtil";
|
||||
import { getPasswordRequirements } from "@/utils/passwordUtil";
|
||||
|
||||
type SetPasswordProps = {
|
||||
show: boolean;
|
||||
@@ -124,12 +119,6 @@ export default function SetPasswordDialog({
|
||||
const password = form.watch("password");
|
||||
const confirmPassword = form.watch("confirmPassword");
|
||||
|
||||
// Password strength calculation
|
||||
const passwordStrength = useMemo(
|
||||
() => calculatePasswordStrength(password),
|
||||
[password],
|
||||
);
|
||||
|
||||
const requirements = useMemo(
|
||||
() => getPasswordRequirements(password),
|
||||
[password],
|
||||
@@ -303,6 +292,12 @@ export default function SetPasswordDialog({
|
||||
)}
|
||||
className="h-10 pr-10"
|
||||
autoFocus
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
if (form.getValues("confirmPassword")) {
|
||||
form.trigger("confirmPassword");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -334,21 +329,6 @@ export default function SetPasswordDialog({
|
||||
|
||||
{password && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-secondary-foreground">
|
||||
<div
|
||||
className={`${getPasswordStrengthColor(
|
||||
password,
|
||||
)} transition-all duration-300`}
|
||||
style={{ width: `${passwordStrength * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("users.dialog.form.password.strength.title")}
|
||||
<span className="font-medium">
|
||||
{getPasswordStrengthLabel(password, t)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="space-y-1 rounded-md bg-muted/50 p-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
{t("users.dialog.form.password.requirements.title")}
|
||||
@@ -376,8 +356,6 @@ export default function SetPasswordDialog({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -428,18 +406,25 @@ export default function SetPasswordDialog({
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
{password &&
|
||||
confirmPassword &&
|
||||
password === confirmPassword && (
|
||||
<div className="mt-1 flex items-center gap-1.5 text-xs">
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
<span className="text-green-600">
|
||||
{t("users.dialog.form.password.match")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormMessage />
|
||||
{password && confirmPassword && (
|
||||
<div className="mt-1 flex items-center gap-1.5 text-xs">
|
||||
{password === confirmPassword ? (
|
||||
<>
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
<span className="text-green-600">
|
||||
{t("users.dialog.form.password.match")}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
<span className="text-red-600">
|
||||
{t("users.dialog.form.password.notMatch")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -147,7 +147,18 @@ export default function DetailStream({
|
||||
// away instead of waiting for a future effectiveTime change.
|
||||
useEffect(() => {
|
||||
if (!reviewItems || reviewItems.length === 0) return;
|
||||
if (activeReviewId) return;
|
||||
|
||||
// an id left over from another camera's items counts as unset
|
||||
if (
|
||||
activeReviewId &&
|
||||
reviewItems.some(
|
||||
(r) =>
|
||||
`review-${r.id ?? r.start_time ?? Math.floor(r.start_time ?? 0)}` ===
|
||||
activeReviewId,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target: ReviewSegment | undefined;
|
||||
let closest: { r: ReviewSegment; diff: number } | undefined;
|
||||
|
||||
@@ -1,41 +1,3 @@
|
||||
import { TFunction } from "i18next";
|
||||
|
||||
export const calculatePasswordStrength = (password: string): number => {
|
||||
if (!password) return 0;
|
||||
|
||||
let strength = 0;
|
||||
if (password.length >= 8) strength += 1;
|
||||
if (/\d/.test(password)) strength += 1;
|
||||
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength += 1;
|
||||
if (/[A-Z]/.test(password)) strength += 1;
|
||||
|
||||
return strength;
|
||||
};
|
||||
|
||||
export const getPasswordRequirements = (password: string) => ({
|
||||
length: password?.length >= 12,
|
||||
});
|
||||
|
||||
export const getPasswordStrengthLabel = (
|
||||
password: string,
|
||||
t: TFunction,
|
||||
): string => {
|
||||
const strength = calculatePasswordStrength(password);
|
||||
|
||||
if (!password) return "";
|
||||
if (strength < 1)
|
||||
return t("users.dialog.form.password.strength.weak", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
return t("users.dialog.form.password.strength.veryStrong", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
};
|
||||
|
||||
export const getPasswordStrengthColor = (password: string): string => {
|
||||
const strength = calculatePasswordStrength(password);
|
||||
|
||||
if (!password) return "bg-gray-200";
|
||||
if (strength === 0) return "bg-red-500";
|
||||
return "bg-green-500";
|
||||
};
|
||||
|
||||
@@ -45,8 +45,7 @@ export function createRecordingReviewUrl(
|
||||
pathname: string,
|
||||
state: RecordingReviewLinkState,
|
||||
): string {
|
||||
const url = new URL(baseUrl);
|
||||
url.pathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
||||
const url = new URL(pathname.replace(/^\//, ""), baseUrl);
|
||||
url.searchParams.set(
|
||||
RECORDING_REVIEW_LINK_PARAM,
|
||||
`${state.camera}_${Math.floor(state.timestamp)}`,
|
||||
|
||||
@@ -575,25 +575,19 @@ export default function DraggableGridLayout({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!allGroupsStreamingSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initialAudioStates: AudioState = {};
|
||||
const initialVolumeStates: VolumeState = {};
|
||||
|
||||
Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
|
||||
if (groupSettings) {
|
||||
Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
|
||||
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
|
||||
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.entries(currentGroupStreamingSettings ?? {}).forEach(
|
||||
([camera, cameraSettings]) => {
|
||||
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
|
||||
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
|
||||
},
|
||||
);
|
||||
|
||||
setAudioStates(initialAudioStates);
|
||||
setVolumeStates(initialVolumeStates);
|
||||
}, [allGroupsStreamingSettings]);
|
||||
}, [currentGroupStreamingSettings, cameraGroup]);
|
||||
|
||||
const toggleAudio = (cameraName: string) => {
|
||||
setAudioStates((prev) => ({
|
||||
@@ -800,7 +794,7 @@ export default function DraggableGridLayout({
|
||||
toggleAudio={() => toggleAudio(camera.name)}
|
||||
statsState={statsStates[camera.name]}
|
||||
toggleStats={() => toggleStats(camera.name)}
|
||||
volumeState={volumeStates[camera.name]}
|
||||
volumeState={volumeStates[camera.name] ?? 1}
|
||||
setVolumeState={(value) =>
|
||||
setVolumeStates((prev) => ({
|
||||
...prev,
|
||||
@@ -845,7 +839,7 @@ export default function DraggableGridLayout({
|
||||
onError={(e) => handleError(camera.name, e)}
|
||||
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
||||
playAudio={audioStates[camera.name]}
|
||||
volume={volumeStates[camera.name]}
|
||||
volume={volumeStates[camera.name] ?? 1}
|
||||
onLiveAspectChange={liveAspectHandlers[camera.name]}
|
||||
/>
|
||||
</GridLiveContextMenu>
|
||||
|
||||
@@ -511,7 +511,14 @@ export default function LiveCameraView({
|
||||
}
|
||||
}, [webRTCVerdictPending]);
|
||||
|
||||
useKeyboardListener(["m", "Escape"], (key, modifiers) => {
|
||||
const toggleMic = () => {
|
||||
setMic(!mic);
|
||||
if (!mic && !audio) {
|
||||
setAudio(true);
|
||||
}
|
||||
};
|
||||
|
||||
useKeyboardListener(["m", "t", "Escape"], (key, modifiers) => {
|
||||
if (!modifiers.down) {
|
||||
return true;
|
||||
}
|
||||
@@ -525,7 +532,7 @@ export default function LiveCameraView({
|
||||
break;
|
||||
case "t":
|
||||
if (supports2WayTalk && talkAvailable) {
|
||||
setMic(!mic);
|
||||
toggleMic();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
@@ -792,12 +799,7 @@ export default function LiveCameraView({
|
||||
? t("twoWayTalk.disable", { ns: "views/live" })
|
||||
: t("twoWayTalk.enable", { ns: "views/live" })
|
||||
}
|
||||
onClick={() => {
|
||||
setMic(!mic);
|
||||
if (!mic && !audio) {
|
||||
setAudio(true);
|
||||
}
|
||||
}}
|
||||
onClick={toggleMic}
|
||||
disabled={!cameraEnabled || debug || !talkAvailable}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -326,25 +326,19 @@ export default function LiveDashboardView({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!allGroupsStreamingSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initialAudioStates: AudioState = {};
|
||||
const initialVolumeStates: VolumeState = {};
|
||||
|
||||
Object.entries(allGroupsStreamingSettings).forEach(([_, groupSettings]) => {
|
||||
if (groupSettings) {
|
||||
Object.entries(groupSettings).forEach(([camera, cameraSettings]) => {
|
||||
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
|
||||
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.entries(currentGroupStreamingSettings ?? {}).forEach(
|
||||
([camera, cameraSettings]) => {
|
||||
initialAudioStates[camera] = cameraSettings.playAudio ?? false;
|
||||
initialVolumeStates[camera] = cameraSettings.volume ?? 1;
|
||||
},
|
||||
);
|
||||
|
||||
setAudioStates(initialAudioStates);
|
||||
setVolumeStates(initialVolumeStates);
|
||||
}, [allGroupsStreamingSettings]);
|
||||
}, [currentGroupStreamingSettings, cameraGroup]);
|
||||
|
||||
const toggleAudio = (cameraName: string): void => {
|
||||
setAudioStates((prev) => ({
|
||||
@@ -635,7 +629,7 @@ export default function LiveDashboardView({
|
||||
resetPreferredLiveMode(camera.name)
|
||||
}
|
||||
playAudio={audioStates[camera.name] ?? false}
|
||||
volume={volumeStates[camera.name]}
|
||||
volume={volumeStates[camera.name] ?? 1}
|
||||
/>
|
||||
</LiveContextMenu>
|
||||
);
|
||||
|
||||
@@ -86,17 +86,21 @@ export default function MediaSyncSettingsView() {
|
||||
position: "top-center",
|
||||
closeButton: true,
|
||||
});
|
||||
} else if (response.status === 409) {
|
||||
toast.error(t("maintenance.sync.alreadyRunning"), {
|
||||
}
|
||||
} catch (error) {
|
||||
const alreadyRunning =
|
||||
axios.isAxiosError(error) && error.response?.status === 409;
|
||||
toast.error(
|
||||
t(
|
||||
alreadyRunning
|
||||
? "maintenance.sync.alreadyRunning"
|
||||
: "maintenance.sync.error",
|
||||
),
|
||||
{
|
||||
position: "top-center",
|
||||
closeButton: true,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
toast.error(t("maintenance.sync.error"), {
|
||||
position: "top-center",
|
||||
closeButton: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
@@ -133,6 +133,9 @@ export default function TriggerView({
|
||||
|
||||
// watch websocket for updates
|
||||
const { payload: triggers_status_ws } = useTriggers();
|
||||
const highlightTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>(
|
||||
{},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!triggers_status_ws) return;
|
||||
@@ -148,13 +151,13 @@ export default function TriggerView({
|
||||
return current;
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const name = triggers_status_ws.name;
|
||||
clearTimeout(highlightTimers.current[name]);
|
||||
highlightTimers.current[name] = setTimeout(() => {
|
||||
setTriggeredTrigger((prev) =>
|
||||
(prev || []).filter((name) => name !== triggers_status_ws.name),
|
||||
(prev || []).filter((triggered) => triggered !== name),
|
||||
);
|
||||
}, 3000);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [triggers_status_ws, selectedCamera, mutate]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user