Compare commits

..
Author SHA1 Message Date
Josh Hawkins b92ad23116 unmute camera audio when starting two-way talk from the shortcut 2026-11-01 08:30:33 -06:00
Josh Hawkins 72c97f2b7d pick the earliest overlapping preview for preview gif and mp4 2026-11-01 08:29:14 -06:00
Josh Hawkins fac65fdca2 show already running message when a media sync is in progress 2026-11-01 08:18:34 -06:00
Josh Hawkins 309a4e5aa8 fix two-way talk shortcut and detail stream active item after camera switch 2026-11-01 08:15:25 -06:00
Josh Hawkins f246db3f4e Hold pandas 3.1 until numpy 2 2026-11-01 07:43:06 -06:00
Josh Hawkins 9fca3f276d address review feedback
- Discard the export row when an export fails before or during ffmpeg, so it doesn't stay in progress until a restart
- Skip the empty thumbnail path when discarding a failed export
- Measure preview clip duration from the clamped start
- Default live player volume to 1 when the group has no saved setting
- Clear the unsaved registration message when notification registration fails
- Keep a highlight timer per trigger
- Remove the severity order_by in /review, which the start_time order_by replaced
2026-11-01 07:29:16 -06:00
Josh Hawkins f68f360386 fix live audio settings and trigger highlights
- start each camera group's audio from that group's saved settings instead of merging every group
- don't clear the previous trigger's highlight timeout when another trigger fires
2026-10-31 22:45:47 -05:00
Josh Hawkins 5764eaddaa miscellaneous fixes
- keep base path in recording share links
- reject non-positive scale on recordings/unavailable, which looped forever on the event loop
- reject negative vod event padding, which cleared has_clip
- clamp negative preview gif/mp4 offsets
- don't delete exports that are still in progress
- hash on unknown login usernames so timing doesn't reveal valid users
- include the first minute of the events time-of-day filter
- show an error when the push subscription is refused
- don't wipe an unnamed known plates entry when Add is pressed again
- use elapsed hours for export last N hours so DST doesn't shift the range
2026-10-31 21:11:15 -05:00
32 changed files with 347 additions and 314 deletions
+1 -1
View File
@@ -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@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
+3
View File
@@ -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"]
+1 -1
View File
@@ -590,7 +590,7 @@ jobs:
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
+1 -1
View File
@@ -1,4 +1,4 @@
ruff == 0.17.3
ruff == 0.17.1
# types
types-peewee == 4.0.*
+5 -5
View File
@@ -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.1
mypy == 2.4.0
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.6
tzdata == 2026.5
pyzmq == 27.2.*
ruamel.yaml == 0.19.*
tzlocal == 5.4.5
@@ -45,9 +45,9 @@ scipy == 1.17.*
openvino == 2026.4.*
onnxruntime == 1.31.*
# Embeddings
transformers == 5.21.*
transformers == 5.20.*
# Generative AI
google-genai == 2.34.*
google-genai == 2.32.*
ollama == 0.6.*
openai == 3.36.*
# push notifications
@@ -68,7 +68,7 @@ prometheus-client == 0.26.*
tflite_runtime @ https://github.com/frigate-nvr/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_x86_64.whl; platform_machine == 'x86_64'
tflite_runtime @ https://github.com/feranick/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_aarch64.whl; platform_machine == 'aarch64'
# audio transcription
sherpa-onnx==2.0.*
sherpa-onnx==1.13.*
faster-whisper==1.2.*
librosa==0.11.*
soundfile==0.14.*
+2
View File
@@ -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:
+4
View File
@@ -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):
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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,
+9 -5
View File
@@ -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",
+1 -2
View File
@@ -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()
+2 -2
View File
@@ -386,7 +386,7 @@ class GeminiClient(GenAIClient):
response = self.provider.models.generate_content(
model=self.genai_config.model,
contents=gemini_messages,
contents=gemini_messages, # type: ignore[arg-type]
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,
contents=gemini_messages, # type: ignore[arg-type]
config=types.GenerateContentConfig(**config_params),
)
+6 -1
View File
@@ -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
+8 -2
View File
@@ -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)
+24
View File
@@ -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")
+4
View File
@@ -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.
+6 -7
View File
@@ -97,13 +97,12 @@ test.describe("setup wizard account @high @mobile", () => {
.fill("a-long-enough-password");
await page.getByRole("button", { name: "Save" }).click();
await expect
.poll(() => sent.find((call) => call.method === "POST")?.body)
.toEqual({
username: "family",
password: "a-long-enough-password",
role: "viewer",
});
const createCall = sent.find((call) => call.method === "POST");
expect(createCall?.body).toEqual({
username: "family",
password: "a-long-enough-password",
role: "viewer",
});
});
test("shows anonymous copy when nobody is signed in", async ({
+11 -23
View File
@@ -101,14 +101,10 @@ test.describe("setup wizard hardware @high @mobile", () => {
await expect(page.getByText("Will use VAAPI (Intel/AMD)")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
await expect
.poll(
() =>
saves.find((save) => save.config_data?.ffmpeg)?.config_data?.ffmpeg,
)
.toEqual({
hwaccel_args: "preset-vaapi",
});
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({
hwaccel_args: "preset-vaapi",
});
// the saved steps only take effect after a restart
const restarts = await captureRestarts(page);
@@ -121,7 +117,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
await page.getByRole("button", { name: "Apply & Restart" }).click();
await expect(page.getByText("Starting Frigate...")).toBeVisible();
await expect.poll(() => restarts).toHaveLength(1);
expect(restarts).toHaveLength(1);
});
test("defers model setup for onnx hardware without Frigate+", async ({
@@ -232,14 +228,10 @@ 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
await expect
.poll(
() =>
saves.find((save) => save.config_data?.ffmpeg)?.config_data?.ffmpeg,
)
.toEqual({
hwaccel_args: "preset-jetson-h264",
});
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({
hwaccel_args: "preset-jetson-h264",
});
});
test("None writes an explicit empty hwaccel list", async ({
@@ -257,11 +249,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
await page.getByRole("radio", { name: "None (software decoding)" }).click();
await page.getByRole("button", { name: "Next" }).click();
await expect
.poll(
() =>
saves.find((save) => save.config_data?.ffmpeg)?.config_data?.ffmpeg,
)
.toEqual({ hwaccel_args: [] });
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({ hwaccel_args: [] });
});
});
+157 -153
View File
@@ -9,8 +9,8 @@
"version": "0.0.0",
"hasInstallScript": true,
"dependencies": {
"@cycjimmy/jsmpeg-player": "^6.1.6",
"@hookform/resolvers": "^6.0.2",
"@cycjimmy/jsmpeg-player": "^6.1.2",
"@hookform/resolvers": "^6.0.0",
"@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.5.0",
"axios": "^1.21.0",
"apexcharts": "^8.4.1",
"axios": "^1.20.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.7.1",
"hls.js": "^1.7.4",
"i18next": "^26.5.11",
"i18next-http-backend": "^4.0.6",
"framer-motion": "^14.6.0",
"hls.js": "^1.7.3",
"i18next": "^26.5.5",
"i18next-http-backend": "^4.0.4",
"idb-keyval": "^6.3.0",
"js-yaml": "^5.5.0",
"konva": "^10.9.0",
"lodash": "^4.18.1",
"lucide-react": "^1.64.0",
"lucide-react": "^1.61.0",
"monaco-yaml": "^5.5.1",
"next-themes": "^0.4.6",
"nosleep.js": "^0.12.0",
"react": "^19.3.1",
"react-apexcharts": "^2.1.2",
"react-apexcharts": "^2.1.1",
"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.91.0",
"react-i18next": "^17.1.9",
"react-hook-form": "^7.90.0",
"react-i18next": "^17.1.1",
"react-icons": "^5.7.0",
"react-konva": "^19.3.2",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.18.5",
"react-router-dom": "^7.18.4",
"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.54.0",
"virtua": "^0.53.6",
"vite-plugin-monaco-editor": "^1.1.0",
"zod": "^4.6.7"
},
@@ -90,11 +90,11 @@
"@playwright/test": "^1.64.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.27",
"@types/node": "^26.6.6",
"@types/node": "^26.6.5",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2",
"autoprefixer": "^10.6.2",
"autoprefixer": "^10.6.1",
"esbuild": "^0.28.2",
"eslint": "^10.13.0",
"eslint-config-prettier": "^10.1.8",
@@ -105,12 +105,12 @@
"i18next-cli": "^1.5.11",
"monaco-editor": "^0.53.0",
"patch-package": "^8.0.1",
"postcss": "^8.5.32",
"postcss": "^8.5.31",
"prettier": "^3.9.10",
"prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^3.4.19",
"typescript": "^6.0.3",
"typescript-eslint": "^8.72.1",
"typescript-eslint": "^8.72.0",
"vite": "^8.3.5"
}
},
@@ -428,9 +428,9 @@
}
},
"node_modules/@cycjimmy/jsmpeg-player": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.1.6.tgz",
"integrity": "sha512-D3ZfarlwDAltkBzmYaAAvARqRvgN4DmM70fbYwRECh9gpT8IRzLSxsXcR0uoPj/W3VtFeQjwA1QBj+rDpXiUzA==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.1.2.tgz",
"integrity": "sha512-U9DBDe5fxHmbwQww9rFxMLNI2Wlg7DhPzI7AVFpq8GehiUP7+NwuMPXpP4zAd52sgkxtOqOeMjgE5g0ZLnQZ0w==",
"license": "MIT"
},
"node_modules/@date-fns/tz": {
@@ -1059,9 +1059,9 @@
"license": "MIT"
},
"node_modules/@hookform/resolvers": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-6.0.2.tgz",
"integrity": "sha512-e2dqUOlaGuirKVOd/iZWk/Yg5zeaGw7t0hSc3LzxL3o7eZGwZkTyEbBL8H52m0wRKEsNLWoMGOiXGHbPHTXlEQ==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-6.0.0.tgz",
"integrity": "sha512-6iBzQnB+YwAveVc8HUi6QYLITn5X+xNKRMFIcNXHZlvp2aTI7yWbj4n/JodXxirMCcBc4VCjb2aYAJh4x07mVw==",
"license": "MIT",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
@@ -1069,6 +1069,7 @@
"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",
@@ -1099,6 +1100,9 @@
"@standard-schema/spec": {
"optional": true
},
"@typeschema/main": {
"optional": true
},
"@vinejs/vine": {
"optional": true
},
@@ -3607,9 +3611,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.6.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
"integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
"version": "26.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
"integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3657,17 +3661,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"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==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@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",
"@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",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -3680,15 +3684,15 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.72.1",
"@typescript-eslint/parser": "^8.72.0",
"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.14",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
"integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"version": "7.0.13",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
"integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3696,16 +3700,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz",
"integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@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",
"debug": "^4.4.3"
},
"engines": {
@@ -3721,14 +3725,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"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==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz",
"integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.72.1",
"@typescript-eslint/types": "^8.72.1",
"@typescript-eslint/tsconfig-utils": "^8.72.0",
"@typescript-eslint/types": "^8.72.0",
"debug": "^4.4.3"
},
"engines": {
@@ -3743,14 +3747,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"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==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz",
"integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1"
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/visitor-keys": "8.72.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3761,9 +3765,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"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==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz",
"integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3778,15 +3782,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz",
"integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz",
"integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1",
"@typescript-eslint/utils": "8.72.1",
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0",
"@typescript-eslint/utils": "8.72.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -3803,9 +3807,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz",
"integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz",
"integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3817,16 +3821,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz",
"integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@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",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -3845,16 +3849,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"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==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz",
"integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1"
"@typescript-eslint/scope-manager": "8.72.0",
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3869,13 +3873,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz",
"integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz",
"integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/types": "8.72.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -4063,9 +4067,9 @@
"license": "SEE LICENSE IN LICENSE.md"
},
"node_modules/apexcharts": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-8.5.0.tgz",
"integrity": "sha512-mH9QiOwIeRGg635OzAc2mznuwrK9XHr9S1RcC26GNNOU6fUPe64cMc7hXGcM9bXz92nO4Ax7P6uxSYaTA7gBDw==",
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-8.4.1.tgz",
"integrity": "sha512-29IbZ1rn3NRkDvete5EzZXy6yDSW7mjNXkytLpvaYHk0XbfsR+FvC7U78L3wMvJa699NBmix2z6r3Qm5YDvw0A==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"apex-commons": "^0.8.3"
@@ -4122,9 +4126,9 @@
}
},
"node_modules/autoprefixer": {
"version": "10.6.2",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.2.tgz",
"integrity": "sha512-VOw9/yGTGwPPgzFcODtcv6zhEPIeIWkRSe10L2ri2f5vZ7ID7lCt6jZzRF5/ubgOZ+szYydmEF5AYsA5jxX1tA==",
"version": "10.6.1",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz",
"integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==",
"dev": true,
"funding": [
{
@@ -4159,9 +4163,9 @@
}
},
"node_modules/axios": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.21.0.tgz",
"integrity": "sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==",
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
"integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
@@ -5511,9 +5515,9 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz",
"integrity": "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==",
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
"integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
"funding": [
{
"type": "individual",
@@ -5578,13 +5582,13 @@
}
},
"node_modules/framer-motion": {
"version": "14.7.1",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.7.1.tgz",
"integrity": "sha512-mFDYZxyfeZnpvL/zEQEqLZNYVA/ya47c9TBfZb/PEoNBBVSVvCLkMrCv9U38cuw72W161XENZwZB0+4nIBLEJg==",
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.6.0.tgz",
"integrity": "sha512-MzBgZxGgoHPfSLEXmo4o5XlZvb+nVRFdHtGjVhm6LMIEJKTt/8hNO023euL5mYyzc8llV/hQTfjnN68tNRHalw==",
"license": "MIT",
"dependencies": {
"motion-dom": "14.7.1",
"motion-utils": "14.7.0",
"motion-dom": "14.6.0",
"motion-utils": "14.6.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
@@ -5945,9 +5949,9 @@
}
},
"node_modules/hls.js": {
"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==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.3.tgz",
"integrity": "sha512-MsPlx6yVW4Qv4C7mEVou4/gk/5cN2dVxOTnrNjPSmyH2f0Ln+/UkG10Ax1PE6OvrmTVPKgQYozTtBaPOuCzq7A==",
"license": "Apache-2.0"
},
"node_modules/hookified": {
@@ -5987,9 +5991,9 @@
}
},
"node_modules/i18next": {
"version": "26.5.11",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.11.tgz",
"integrity": "sha512-Wc0klBPKxqZ2tL2qdAnF8xvvnWgr1IMGSY2ikTiAvaFVbz7xiFARtkoU4RidBDw1SyziGROp0qxu6qnJ7l2Wug==",
"version": "26.5.5",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.5.tgz",
"integrity": "sha512-txdaNOlnZCdiTMSlEzl5hUJU8TC9R4J/RCR1G7XrDPbWT2t3mR142QPz84ZuZeXDt3yq+9zmsdPiakxYtmp3fw==",
"funding": [
{
"type": "individual",
@@ -6102,9 +6106,9 @@
}
},
"node_modules/i18next-http-backend": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.6.tgz",
"integrity": "sha512-k5OPDXAQH9CZ2Ju2DDw1ZC4WiYmjKUjOE4lcd51tNQyWjVDfpuwPcE9Y5VzDpz1tR1Z4wxkkj4H+ImxgwbfpeQ==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.4.tgz",
"integrity": "sha512-nG9OQKvUsPiTVUBg1rCGmvmWhzxC9GOxVbGdTYyejHVMSUjutt1hiBNVSzhB/ZE7keL9Xkb880kUfhDovV5T5g==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -6974,9 +6978,9 @@
}
},
"node_modules/lucide-react": {
"version": "1.64.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.64.0.tgz",
"integrity": "sha512-v626F/bcLNvFom6vzztHILtG9tUgXXbgA5AGkd82YkV401+Ko/NHyrWafbPwGofa+6p/EgDJcvEkIwsMH9098w==",
"version": "1.61.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.61.0.tgz",
"integrity": "sha512-v2SQPPPNjnr+BlhL9Spuzmo5yR+hLkftfuE4E3jaxdNPgSH0hJKPcLJBP7yAta87jQNPsSERS59PJjCJ/CorBA==",
"license": "ISC",
"peerDependencies": {
"@types/react": "*",
@@ -8051,18 +8055,18 @@
}
},
"node_modules/motion-dom": {
"version": "14.7.1",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.7.1.tgz",
"integrity": "sha512-p9q9F7+HoijbQjUG/cSGNmWS2t1OaAKxkd2RG5gQJRoDwoTap/Khb7K1P5rLW90crnSmx0rcRXzh3Jd5UNrpEw==",
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.6.0.tgz",
"integrity": "sha512-lIojTl9aYSPIkeOOSzBliy1nNvpdO3JtVzW2WgTXQlOP4ZMuJhtWO3fP7OD4TVFwlRucdvxlDSMkL+fEQnQ52A==",
"license": "MIT",
"dependencies": {
"motion-utils": "14.7.0"
"motion-utils": "14.6.0"
}
},
"node_modules/motion-utils": {
"version": "14.7.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.7.0.tgz",
"integrity": "sha512-woO0/Nb7ys75f94fjvQi/6leiHGvZUYkeHUPEZ3vxsV+9OpEVhkxp01x8MlRwX0zt31EqgXsKDAoBoJLC148Tw==",
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.6.0.tgz",
"integrity": "sha512-bZJo2AmQ2aOFkaato6VXrTbkPG0XhMzivWUWSaI59pxyllj8QMQUFZ84MYRQnPkK+7dr4KldyJwF2+RM0sdq/w==",
"license": "MIT"
},
"node_modules/ms": {
@@ -8544,9 +8548,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
"integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
"version": "8.5.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
"integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
"funding": [
{
"type": "opencollective",
@@ -8916,10 +8920,10 @@
}
},
"node_modules/react-apexcharts": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-2.1.2.tgz",
"integrity": "sha512-vIoltFDOxy/EIdyWam833mgEfZiOQ+R6oqCvRgbGqNNTNNITCKL91CNeXThi2L8+YyxbamxyWY/6aZV66mx1fA==",
"license": "SEE LICENSE IN LICENSE.md",
"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",
"dependencies": {
"prop-types": "^15.8.1"
},
@@ -9033,9 +9037,9 @@
}
},
"node_modules/react-hook-form": {
"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==",
"version": "7.90.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.90.0.tgz",
"integrity": "sha512-+NdG0FLzCPhFOp2NNxaUYLF9fyZhNjx9FTBBSYWF71/l6BBlmIc/j6xlp3DbaHWLJbIG7Zd8oznA19xJClU97w==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
@@ -9055,9 +9059,9 @@
}
},
"node_modules/react-i18next": {
"version": "17.1.9",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.1.9.tgz",
"integrity": "sha512-OgYqIH13XHPlWTaaI795Dz+k2AdKnXcOxNQVnf750T8Ij0TYWp1nYvXRpoJBulPqlyk2OAbn1PM4tFKiIj/byA==",
"version": "17.1.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.1.1.tgz",
"integrity": "sha512-Q76xpzxrZNcqhB7M31Ns7isulGOlmhURqRsJOGlYgJJCyoy1pMq6tRl8KQycGNNY1vPaxbIdrogrqY9nkb+SEA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.7",
@@ -9231,9 +9235,9 @@
}
},
"node_modules/react-router": {
"version": "7.18.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.5.tgz",
"integrity": "sha512-dTgvcXDOrLPrlLHrcZAqWveiK1hMSqN+rGAJzegP8HtmNkfRxOoshGWBNX3ZJ0cZjOHkRdlUJtaIhZMLLyGyPw==",
"version": "7.18.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz",
"integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -9253,12 +9257,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.18.5",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.5.tgz",
"integrity": "sha512-9HcfDMU6JJ/fp6gJZYGwNE6RknVGS4kBjH1HkNw959mCJi70TVwdipQWv2FEol9uGpi+SZTFtVpJPtEQfayeOw==",
"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==",
"license": "MIT",
"dependencies": {
"react-router": "7.18.5"
"react-router": "7.18.4"
},
"engines": {
"node": ">=20.0.0"
@@ -9582,9 +9586,9 @@
}
},
"node_modules/set-cookie-parser": {
"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==",
"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==",
"license": "MIT"
},
"node_modules/set-function-length": {
@@ -10058,16 +10062,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz",
"integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.0.tgz",
"integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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"
"@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"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -10360,9 +10364,9 @@
}
},
"node_modules/virtua": {
"version": "0.54.0",
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.54.0.tgz",
"integrity": "sha512-8nGGrDhX61MDVeqgyIaWrJqhRm9z2Nqa1WDqt0tk1m+ZWrvLesMP0iL0vL6xNsLCF2jE2ohwY+PqufdbVmRgEw==",
"version": "0.53.6",
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.53.6.tgz",
"integrity": "sha512-/vQtSFNDFcp3Yz4klbpe8Z88eQeThSw49cFlgJQdsqh3SUnRlW4jLi6ZYt4YzdwerYGlP6KCgAOXNVO+PmUg7w==",
"license": "MIT",
"peerDependencies": {
"@angular/common": ">=20.0.0",
+18 -18
View File
@@ -21,8 +21,8 @@
"i18n:status": "i18next-cli status"
},
"dependencies": {
"@cycjimmy/jsmpeg-player": "^6.1.6",
"@hookform/resolvers": "^6.0.2",
"@cycjimmy/jsmpeg-player": "^6.1.2",
"@hookform/resolvers": "^6.0.0",
"@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.5.0",
"axios": "^1.21.0",
"apexcharts": "^8.4.1",
"axios": "^1.20.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.7.1",
"hls.js": "^1.7.4",
"i18next": "^26.5.11",
"i18next-http-backend": "^4.0.6",
"framer-motion": "^14.6.0",
"hls.js": "^1.7.3",
"i18next": "^26.5.5",
"i18next-http-backend": "^4.0.4",
"idb-keyval": "^6.3.0",
"js-yaml": "^5.5.0",
"konva": "^10.9.0",
"lodash": "^4.18.1",
"lucide-react": "^1.64.0",
"lucide-react": "^1.61.0",
"monaco-yaml": "^5.5.1",
"next-themes": "^0.4.6",
"nosleep.js": "^0.12.0",
"react": "^19.3.1",
"react-apexcharts": "^2.1.2",
"react-apexcharts": "^2.1.1",
"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.91.0",
"react-i18next": "^17.1.9",
"react-hook-form": "^7.90.0",
"react-i18next": "^17.1.1",
"react-icons": "^5.7.0",
"react-konva": "^19.3.2",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.18.5",
"react-router-dom": "^7.18.4",
"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.54.0",
"virtua": "^0.53.6",
"vite-plugin-monaco-editor": "^1.1.0",
"zod": "^4.6.7"
},
@@ -102,11 +102,11 @@
"@playwright/test": "^1.64.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.27",
"@types/node": "^26.6.6",
"@types/node": "^26.6.5",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2",
"autoprefixer": "^10.6.2",
"autoprefixer": "^10.6.1",
"esbuild": "^0.28.2",
"eslint": "^10.13.0",
"eslint-config-prettier": "^10.1.8",
@@ -117,12 +117,12 @@
"i18next-cli": "^1.5.11",
"monaco-editor": "^0.53.0",
"patch-package": "^8.0.1",
"postcss": "^8.5.32",
"postcss": "^8.5.31",
"prettier": "^3.9.10",
"prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^3.4.19",
"typescript": "^6.0.3",
"typescript-eslint": "^8.72.1",
"typescript-eslint": "^8.72.0",
"vite": "^8.3.5"
},
"overrides": {
@@ -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]);
+2 -30
View File
@@ -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,
+12 -1
View File
@@ -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 -2
View File
@@ -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)}`,
+9 -15
View File
@@ -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>
+10 -8
View File
@@ -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}
/>
)}
+8 -14
View File
@@ -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);
}
+8 -5
View File
@@ -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(() => {