diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index ea95f93..ce31a92 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -26,14 +26,19 @@ jobs:
- name: Build frontend
working-directory: ui
run: pnpm run build
- - name: Set up Python
- uses: actions/setup-python@v6
- with:
- python-version: '3.13'
+ - name: Run frontend tests
+ working-directory: ui
+ run: pnpm exec ng test --watch=false
+ env:
+ CI: true
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ - name: Install Python dependencies
+ run: uv sync --frozen --group dev
- name: Run backend smoke checks
run: python -m compileall app
- name: Run backend tests
- run: python -m unittest discover -s app/tests -p "test_*.py"
+ run: uv run pytest app/tests/
- name: Run Trivy filesystem scan
uses: aquasecurity/trivy-action@0.35.0
with:
diff --git a/app/main.py b/app/main.py
index ff31190..db7be18 100644
--- a/app/main.py
+++ b/app/main.py
@@ -623,14 +623,14 @@ def get_custom_dirs():
return result
@routes.get(config.URL_PREFIX)
-def index(request):
+async def index(request):
response = web.FileResponse(os.path.join(config.BASE_DIR, 'ui/dist/metube/browser/index.html'))
if 'metube_theme' not in request.cookies:
response.set_cookie('metube_theme', config.DEFAULT_THEME)
return response
@routes.get(config.URL_PREFIX + 'robots.txt')
-def robots(request):
+async def robots(request):
if config.ROBOTS_TXT:
response = web.FileResponse(os.path.join(config.BASE_DIR, config.ROBOTS_TXT))
else:
@@ -640,7 +640,7 @@ def robots(request):
return response
@routes.get(config.URL_PREFIX + 'version')
-def version(request):
+async def version(request):
return web.json_response({
"yt-dlp": yt_dlp_version,
"version": os.getenv("METUBE_VERSION", "dev")
@@ -648,11 +648,11 @@ def version(request):
if config.URL_PREFIX != '/':
@routes.get('/')
- def index_redirect_root(request):
+ async def index_redirect_root(request):
return web.HTTPFound(config.URL_PREFIX)
@routes.get(config.URL_PREFIX[:-1])
- def index_redirect_dir(request):
+ async def index_redirect_dir(request):
return web.HTTPFound(config.URL_PREFIX)
routes.static(config.URL_PREFIX + 'download/', config.DOWNLOAD_DIR, show_index=config.DOWNLOAD_DIRS_INDEXABLE)
diff --git a/app/tests/conftest.py b/app/tests/conftest.py
new file mode 100644
index 0000000..114806b
--- /dev/null
+++ b/app/tests/conftest.py
@@ -0,0 +1,32 @@
+"""Pytest configuration: set env and filesystem layout before importing ``main``."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+from pathlib import Path
+
+
+def _ensure_test_env() -> None:
+ if os.environ.get("METUBE_TEST_ENV_READY"):
+ return
+ tmp = tempfile.mkdtemp(prefix="metube-pytest-")
+ base = Path(tmp)
+ browser = base / "ui" / "dist" / "metube" / "browser"
+ browser.mkdir(parents=True)
+ (browser / "index.html").write_text("
", encoding="utf-8")
+ dl = base / "downloads"
+ st = base / "state"
+ dl.mkdir(parents=True)
+ st.mkdir(parents=True)
+ os.environ["DOWNLOAD_DIR"] = str(dl)
+ os.environ["STATE_DIR"] = str(st)
+ os.environ["TEMP_DIR"] = str(dl)
+ os.environ["YTDL_OPTIONS"] = "{}"
+ os.environ["YTDL_OPTIONS_FILE"] = ""
+ os.environ["BASE_DIR"] = str(base)
+ os.environ["LOGLEVEL"] = "INFO"
+ os.environ["METUBE_TEST_ENV_READY"] = "1"
+
+
+_ensure_test_env()
diff --git a/app/tests/test_api.py b/app/tests/test_api.py
new file mode 100644
index 0000000..4aa18e8
--- /dev/null
+++ b/app/tests/test_api.py
@@ -0,0 +1,207 @@
+"""HTTP handler tests for ``main`` using mocked ``web.Request`` (no TestServer)."""
+
+from __future__ import annotations
+
+import json
+from unittest.mock import AsyncMock, MagicMock
+
+import pytest
+from aiohttp import web
+
+import main
+
+
+@pytest.fixture
+def mock_dqueue(monkeypatch):
+ d = MagicMock()
+ d.initialize = AsyncMock(return_value=None)
+ d.add = AsyncMock(return_value={"status": "ok"})
+ d.cancel = AsyncMock(return_value={"status": "ok"})
+ d.start_pending = AsyncMock(return_value={"status": "ok"})
+ d.cancel_add = MagicMock()
+ d.queue = MagicMock()
+ d.done = MagicMock()
+ d.pending = MagicMock()
+ d.queue.saved_items = MagicMock(return_value=[])
+ d.done.saved_items = MagicMock(return_value=[])
+ d.pending.saved_items = MagicMock(return_value=[])
+ d.get = MagicMock(return_value=([], []))
+ monkeypatch.setattr(main, "dqueue", d)
+ return d
+
+
+def _valid_video_add_body(**kwargs):
+ base = {
+ "url": "https://example.com/watch?v=1",
+ "download_type": "video",
+ "codec": "auto",
+ "format": "any",
+ "quality": "best",
+ }
+ base.update(kwargs)
+ return base
+
+
+def _json_request(body: dict | None):
+ req = MagicMock(spec=web.Request)
+ req.json = AsyncMock(return_value=body)
+ return req
+
+
+@pytest.mark.asyncio
+async def test_add_ok(mock_dqueue):
+ req = _json_request(_valid_video_add_body())
+ resp = await main.add(req)
+ assert resp.status == 200
+ text = resp.text
+ data = json.loads(text)
+ assert data["status"] == "ok"
+ mock_dqueue.add.assert_awaited_once()
+
+
+@pytest.mark.asyncio
+async def test_add_missing_url_returns_400(mock_dqueue):
+ req = _json_request({"download_type": "video", "quality": "best", "format": "any"})
+ with pytest.raises(web.HTTPBadRequest):
+ await main.add(req)
+ mock_dqueue.add.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_add_invalid_download_type(mock_dqueue):
+ req = _json_request(_valid_video_add_body(download_type="invalid"))
+ with pytest.raises(web.HTTPBadRequest):
+ await main.add(req)
+
+
+@pytest.mark.asyncio
+async def test_add_invalid_video_quality(mock_dqueue):
+ req = _json_request(_valid_video_add_body(quality="9999"))
+ with pytest.raises(web.HTTPBadRequest):
+ await main.add(req)
+
+
+@pytest.mark.asyncio
+async def test_add_invalid_subtitle_language(mock_dqueue):
+ req = _json_request(
+ {
+ "url": "https://example.com/v",
+ "download_type": "captions",
+ "codec": "auto",
+ "format": "srt",
+ "quality": "best",
+ "subtitle_language": "bad language!",
+ }
+ )
+ with pytest.raises(web.HTTPBadRequest):
+ await main.add(req)
+
+
+@pytest.mark.asyncio
+async def test_add_custom_name_prefix_path_traversal(mock_dqueue):
+ req = _json_request(_valid_video_add_body(custom_name_prefix="../evil"))
+ with pytest.raises(web.HTTPBadRequest):
+ await main.add(req)
+
+
+@pytest.mark.asyncio
+async def test_add_chapter_template_path_traversal(mock_dqueue):
+ req = _json_request(
+ _valid_video_add_body(
+ split_by_chapters=True,
+ chapter_template="/etc/passwd%(title)s",
+ )
+ )
+ with pytest.raises(web.HTTPBadRequest):
+ await main.add(req)
+
+
+@pytest.mark.asyncio
+async def test_add_invalid_json_body(mock_dqueue):
+ req = MagicMock(spec=web.Request)
+ req.json = AsyncMock(side_effect=json.JSONDecodeError("msg", "", 0))
+ with pytest.raises(web.HTTPBadRequest):
+ await main.add(req)
+
+
+@pytest.mark.asyncio
+async def test_delete_missing_ids(mock_dqueue):
+ req = _json_request({"where": "queue"})
+ with pytest.raises(web.HTTPBadRequest):
+ await main.delete(req)
+
+
+@pytest.mark.asyncio
+async def test_delete_queue_calls_cancel(mock_dqueue):
+ req = _json_request({"where": "queue", "ids": ["http://x"]})
+ resp = await main.delete(req)
+ assert resp.status == 200
+ mock_dqueue.cancel.assert_awaited_once_with(["http://x"])
+
+
+@pytest.mark.asyncio
+async def test_start_pending(mock_dqueue):
+ req = _json_request({"ids": ["a"]})
+ resp = await main.start(req)
+ assert resp.status == 200
+ mock_dqueue.start_pending.assert_awaited_once_with(["a"])
+
+
+@pytest.mark.asyncio
+async def test_history_shape(mock_dqueue):
+ mock_dqueue.queue.saved_items.return_value = []
+ mock_dqueue.done.saved_items.return_value = []
+ mock_dqueue.pending.saved_items.return_value = []
+ req = MagicMock(spec=web.Request)
+ resp = await main.history(req)
+ assert resp.status == 200
+ data = json.loads(resp.text)
+ assert set(data.keys()) == {"done", "queue", "pending"}
+
+
+@pytest.mark.asyncio
+async def test_version_json(mock_dqueue):
+ req = MagicMock(spec=web.Request)
+ resp = await main.version(req)
+ assert resp.status == 200
+ body = json.loads(resp.text)
+ assert "yt-dlp" in body and "version" in body
+
+
+@pytest.mark.asyncio
+async def test_cookie_status(mock_dqueue):
+ req = MagicMock(spec=web.Request)
+ resp = await main.cookie_status(req)
+ assert resp.status == 200
+ data = json.loads(resp.text)
+ assert data.get("status") == "ok"
+ assert "has_cookies" in data
+
+
+@pytest.mark.asyncio
+async def test_options_add_cors(mock_dqueue):
+ req = MagicMock(spec=web.Request)
+ resp = await main.add_cors(req)
+ assert resp.status == 200
+
+
+@pytest.mark.asyncio
+async def test_upload_cookies_missing_field(mock_dqueue):
+ req = MagicMock(spec=web.Request)
+ reader = MagicMock()
+ field = MagicMock()
+ field.name = "wrongname"
+ reader.next = AsyncMock(side_effect=[field, None])
+ req.multipart = AsyncMock(return_value=reader)
+ resp = await main.upload_cookies(req)
+ assert resp.status == 400
+
+
+@pytest.mark.asyncio
+async def test_add_legacy_format_migrated(mock_dqueue):
+ req = _json_request({"url": "https://example.com/v", "format": "m4a", "quality": "best"})
+ resp = await main.add(req)
+ assert resp.status == 200
+ call = mock_dqueue.add.await_args
+ assert call is not None
+ assert call.args[1] == "audio"
diff --git a/app/tests/test_config.py b/app/tests/test_config.py
new file mode 100644
index 0000000..0461ba1
--- /dev/null
+++ b/app/tests/test_config.py
@@ -0,0 +1,78 @@
+"""Tests for ``Config`` (env parsing, yt-dlp options, frontend_safe)."""
+
+from __future__ import annotations
+
+import json
+import os
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from main import Config
+
+
+def _base_env(**overrides: str) -> dict[str, str]:
+ env = {k: str(v) for k, v in Config._DEFAULTS.items()}
+ env.update(overrides)
+ return env
+
+
+class ConfigTests(unittest.TestCase):
+ def test_url_prefix_gets_trailing_slash(self):
+ with patch.dict(os.environ, _base_env(URL_PREFIX="foo"), clear=False):
+ c = Config()
+ self.assertEqual(c.URL_PREFIX, "foo/")
+
+ def test_ytdl_options_json_loaded(self):
+ opts = {"quiet": True, "no_warnings": True}
+ with patch.dict(
+ os.environ,
+ _base_env(YTDL_OPTIONS=json.dumps(opts)),
+ clear=False,
+ ):
+ c = Config()
+ self.assertEqual(c.YTDL_OPTIONS["quiet"], True)
+
+ def test_invalid_ytdl_options_exits(self):
+ with patch.dict(os.environ, _base_env(YTDL_OPTIONS="not-json"), clear=False):
+ with self.assertRaises(SystemExit):
+ Config()
+
+ def test_invalid_boolean_env_exits(self):
+ with patch.dict(os.environ, _base_env(CUSTOM_DIRS="maybe"), clear=False):
+ with self.assertRaises(SystemExit):
+ Config()
+
+ def test_frontend_safe_excludes_secrets(self):
+ with patch.dict(os.environ, _base_env(), clear=False):
+ c = Config()
+ safe = c.frontend_safe()
+ self.assertNotIn("YTDL_OPTIONS", safe)
+ self.assertNotIn("HOST", safe)
+
+ def test_runtime_override_roundtrip(self):
+ with patch.dict(os.environ, _base_env(), clear=False):
+ c = Config()
+ c.set_runtime_override("cookiefile", "/tmp/c.txt")
+ self.assertEqual(c.YTDL_OPTIONS.get("cookiefile"), "/tmp/c.txt")
+ c.remove_runtime_override("cookiefile")
+ self.assertIsNone(c.YTDL_OPTIONS.get("cookiefile"))
+
+ def test_ytdl_options_file_merges(self):
+ with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
+ json.dump({"extractor_args": {"youtube": {"player_client": ["web"]}}}, f)
+ path = f.name
+ try:
+ with patch.dict(
+ os.environ,
+ _base_env(YTDL_OPTIONS="{}", YTDL_OPTIONS_FILE=path),
+ clear=False,
+ ):
+ c = Config()
+ self.assertIn("extractor_args", c.YTDL_OPTIONS)
+ finally:
+ os.unlink(path)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/app/tests/test_dl_formats.py b/app/tests/test_dl_formats.py
index 5a1e92f..f1fbf48 100644
--- a/app/tests/test_dl_formats.py
+++ b/app/tests/test_dl_formats.py
@@ -1,6 +1,16 @@
+"""Tests for ``app.dl_formats`` format selectors and yt-dlp option mapping."""
+
+from __future__ import annotations
+
+import copy
import unittest
-from app.dl_formats import get_format, get_opts
+from app.dl_formats import (
+ _normalize_caption_mode,
+ _normalize_subtitle_language,
+ get_format,
+ get_opts,
+)
class DlFormatsTests(unittest.TestCase):
@@ -16,6 +26,114 @@ class DlFormatsTests(unittest.TestCase):
opts = get_opts("audio", "auto", "mp3", "best", {})
self.assertTrue(opts.get("writethumbnail"))
+ def test_custom_format_passthrough(self):
+ self.assertEqual(get_format("video", "auto", "custom:bestvideo+bestaudio", "best"), "bestvideo+bestaudio")
+
+ def test_thumbnail_and_captions_format_strings(self):
+ self.assertEqual(get_format("thumbnail", "auto", "jpg", "best"), "bestaudio/best")
+ self.assertEqual(get_format("captions", "auto", "srt", "best"), "bestaudio/best")
+
+ def test_audio_formats(self):
+ for fmt in ("m4a", "mp3", "opus", "wav", "flac"):
+ with self.subTest(fmt=fmt):
+ self.assertIn(f"ext={fmt}", get_format("audio", "auto", fmt, "best"))
+
+ def test_video_unknown_format_raises(self):
+ with self.assertRaises(ValueError):
+ get_format("video", "auto", "mkv", "best")
+
+ def test_unknown_download_type_raises(self):
+ with self.assertRaises(ValueError):
+ get_format("unknown", "auto", "any", "best")
+
+ def test_video_any_mp4_ios_with_height_quality(self):
+ self.assertIn("height<=1080", get_format("video", "auto", "any", "1080"))
+ self.assertNotIn("height<=", get_format("video", "auto", "any", "best"))
+ self.assertNotIn("height<=", get_format("video", "auto", "any", "worst"))
+
+ def test_video_codec_filters(self):
+ self.assertIn("h264", get_format("video", "h264", "any", "best"))
+ self.assertIn("hevc", get_format("video", "h265", "any", "best"))
+ self.assertIn("av0?1", get_format("video", "av1", "any", "best"))
+ self.assertIn("vp0?9", get_format("video", "vp9", "any", "best"))
+
+ def test_video_mp4_includes_m4a_audio(self):
+ s = get_format("video", "auto", "mp4", "720")
+ self.assertIn("[ext=m4a]", s)
+
+ def test_video_ios_selector_contains_avc_pattern(self):
+ s = get_format("video", "auto", "ios", "best")
+ self.assertIn("h26[45]", s)
+
+ def test_get_opts_deepcopy_does_not_mutate_input(self):
+ base = {"postprocessors": [{"key": "Existing"}]}
+ orig = copy.deepcopy(base)
+ get_opts("audio", "auto", "mp3", "best", base)
+ self.assertEqual(base, orig)
+
+ def test_get_opts_audio_m4a_postprocessors(self):
+ opts = get_opts("audio", "auto", "m4a", "best", {})
+ keys = [p["key"] for p in opts["postprocessors"]]
+ self.assertIn("FFmpegExtractAudio", keys)
+
+ def test_get_opts_audio_mp3_quality_not_best(self):
+ opts = get_opts("audio", "auto", "mp3", "192", {})
+ ext = next(p for p in opts["postprocessors"] if p["key"] == "FFmpegExtractAudio")
+ self.assertEqual(ext["preferredquality"], "192")
+
+ def test_get_opts_thumbnail_skip_download(self):
+ opts = get_opts("thumbnail", "auto", "jpg", "best", {})
+ self.assertTrue(opts.get("skip_download"))
+ self.assertTrue(opts.get("writethumbnail"))
+
+ def test_get_opts_captions_manual_only(self):
+ opts = get_opts(
+ "captions", "auto", "vtt", "best", {}, subtitle_language="fr", subtitle_mode="manual_only"
+ )
+ self.assertTrue(opts.get("writesubtitles"))
+ self.assertFalse(opts.get("writeautomaticsub"))
+ self.assertEqual(opts["subtitleslangs"], ["fr"])
+
+ def test_get_opts_captions_auto_only(self):
+ opts = get_opts(
+ "captions", "auto", "srt", "best", {}, subtitle_language="de", subtitle_mode="auto_only"
+ )
+ self.assertFalse(opts.get("writesubtitles"))
+ self.assertTrue(opts.get("writeautomaticsub"))
+ self.assertEqual(opts["subtitleslangs"], ["de-orig", "de"])
+
+ def test_get_opts_captions_prefer_auto(self):
+ opts = get_opts(
+ "captions", "auto", "srt", "best", {}, subtitle_language="es", subtitle_mode="prefer_auto"
+ )
+ self.assertTrue(opts.get("writesubtitles"))
+ self.assertTrue(opts.get("writeautomaticsub"))
+ self.assertEqual(opts["subtitleslangs"], ["es-orig", "es"])
+
+ def test_get_opts_captions_prefer_manual_default_branch(self):
+ opts = get_opts(
+ "captions", "auto", "srt", "best", {}, subtitle_language="it", subtitle_mode="prefer_manual"
+ )
+ self.assertEqual(opts["subtitleslangs"], ["it", "it-orig"])
+
+ def test_get_opts_captions_txt_maps_to_srt_format(self):
+ opts = get_opts("captions", "auto", "txt", "best", {})
+ self.assertEqual(opts["subtitlesformat"], "srt")
+
+ def test_get_opts_merges_existing_postprocessors(self):
+ opts = get_opts("audio", "auto", "opus", "best", {"postprocessors": [{"key": "SponsorBlock"}]})
+ keys = [p["key"] for p in opts["postprocessors"]]
+ self.assertIn("SponsorBlock", keys)
+ self.assertIn("FFmpegExtractAudio", keys)
+
+ def test_normalize_caption_mode_invalid_defaults(self):
+ self.assertEqual(_normalize_caption_mode(""), "prefer_manual")
+ self.assertEqual(_normalize_caption_mode("not_a_mode"), "prefer_manual")
+
+ def test_normalize_subtitle_language_empty_defaults_en(self):
+ self.assertEqual(_normalize_subtitle_language(""), "en")
+ self.assertEqual(_normalize_subtitle_language(" "), "en")
+
if __name__ == "__main__":
unittest.main()
diff --git a/app/tests/test_download_queue.py b/app/tests/test_download_queue.py
new file mode 100644
index 0000000..36f2da4
--- /dev/null
+++ b/app/tests/test_download_queue.py
@@ -0,0 +1,146 @@
+"""Tests for ``DownloadQueue`` with mocked yt-dlp extraction."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+
+from ytdl import DownloadQueue
+
+
+@pytest.fixture
+def dq_env():
+ with tempfile.TemporaryDirectory() as tmp:
+ dl = os.path.join(tmp, "downloads")
+ st = os.path.join(tmp, "state")
+ os.makedirs(dl, exist_ok=True)
+ os.makedirs(st, exist_ok=True)
+ cfg = MagicMock()
+ cfg.STATE_DIR = st
+ cfg.DOWNLOAD_DIR = dl
+ cfg.AUDIO_DOWNLOAD_DIR = dl
+ cfg.TEMP_DIR = dl
+ cfg.MAX_CONCURRENT_DOWNLOADS = "3"
+ cfg.YTDL_OPTIONS = {}
+ cfg.CUSTOM_DIRS = True
+ cfg.CREATE_CUSTOM_DIRS = True
+ cfg.CLEAR_COMPLETED_AFTER = "0"
+ cfg.DELETE_FILE_ON_TRASHCAN = False
+ cfg.OUTPUT_TEMPLATE = "%(title)s.%(ext)s"
+ cfg.OUTPUT_TEMPLATE_CHAPTER = "%(title)s.%(ext)s"
+ cfg.OUTPUT_TEMPLATE_PLAYLIST = ""
+ cfg.OUTPUT_TEMPLATE_CHANNEL = ""
+ yield cfg
+
+
+def test_cancel_add_increments_generation(dq_env):
+ notifier = MagicMock()
+ dq = DownloadQueue(dq_env, notifier)
+ before = dq._add_generation
+ dq.cancel_add()
+ assert dq._add_generation == before + 1
+
+
+def test_get_returns_tuple_of_lists(dq_env):
+ notifier = MagicMock()
+ dq = DownloadQueue(dq_env, notifier)
+ q, done = dq.get()
+ assert q == [] and done == []
+
+
+@pytest.mark.asyncio
+async def test_add_single_video_goes_to_pending_when_auto_start_false(dq_env):
+ notifier = AsyncMock()
+
+ def fake_extract(self, url):
+ return {
+ "_type": "video",
+ "id": "vid1",
+ "title": "Test Video",
+ "url": url,
+ "webpage_url": url,
+ }
+
+ dq = DownloadQueue(dq_env, notifier)
+ with patch.object(DownloadQueue, "_DownloadQueue__extract_info", fake_extract):
+ result = await dq.add(
+ "https://example.com/watch?v=1",
+ "video",
+ "auto",
+ "any",
+ "best",
+ "",
+ "",
+ 0,
+ auto_start=False,
+ )
+ assert result["status"] == "ok"
+ assert dq.pending.exists("https://example.com/watch?v=1")
+
+
+@pytest.mark.asyncio
+async def test_cancel_removes_from_pending(dq_env):
+ notifier = AsyncMock()
+
+ def fake_extract(self, url):
+ return {
+ "_type": "video",
+ "id": "vid1",
+ "title": "Test Video",
+ "url": url,
+ "webpage_url": url,
+ }
+
+ dq = DownloadQueue(dq_env, notifier)
+ with patch.object(DownloadQueue, "_DownloadQueue__extract_info", fake_extract):
+ await dq.add(
+ "https://example.com/pending",
+ "video",
+ "auto",
+ "any",
+ "best",
+ "",
+ "",
+ 0,
+ auto_start=False,
+ )
+ url = "https://example.com/pending"
+ await dq.cancel([url])
+ assert not dq.pending.exists(url)
+ notifier.canceled.assert_awaited()
+
+
+@pytest.mark.asyncio
+async def test_start_pending_moves_to_queue(dq_env):
+ notifier = AsyncMock()
+
+ def fake_extract(self, url):
+ return {
+ "_type": "video",
+ "id": "vid1",
+ "title": "Test Video",
+ "url": url,
+ "webpage_url": url,
+ }
+
+ dq = DownloadQueue(dq_env, notifier)
+ with patch.object(DownloadQueue, "_DownloadQueue__extract_info", fake_extract):
+ await dq.add(
+ "https://example.com/startme",
+ "video",
+ "auto",
+ "any",
+ "best",
+ "",
+ "",
+ 0,
+ auto_start=False,
+ )
+ url = "https://example.com/startme"
+ # Starting will spawn real download — cancel immediately before worker runs much
+ with patch.object(DownloadQueue, "_DownloadQueue__start_download", AsyncMock()):
+ await dq.start_pending([url])
+ assert not dq.pending.exists(url)
diff --git a/app/tests/test_main_helpers.py b/app/tests/test_main_helpers.py
new file mode 100644
index 0000000..4258b79
--- /dev/null
+++ b/app/tests/test_main_helpers.py
@@ -0,0 +1,105 @@
+"""Tests for pure helpers in ``main`` (legacy API migration, logging, JSON serializer)."""
+
+from __future__ import annotations
+
+import json
+import logging
+import unittest
+
+import main
+
+
+class MigrateLegacyRequestTests(unittest.TestCase):
+ def test_already_new_schema_unchanged(self):
+ post = {"download_type": "video", "codec": "h264", "format": "mp4", "quality": "1080"}
+ before = post.copy()
+ self.assertIs(main._migrate_legacy_request(post), post)
+ self.assertEqual(post, before)
+
+ def test_legacy_audio_m4a(self):
+ post = {"format": "m4a", "quality": "best"}
+ main._migrate_legacy_request(post)
+ self.assertEqual(post["download_type"], "audio")
+ self.assertEqual(post["codec"], "auto")
+ self.assertEqual(post["format"], "m4a")
+
+ def test_legacy_thumbnail(self):
+ post = {"format": "thumbnail", "quality": "best"}
+ main._migrate_legacy_request(post)
+ self.assertEqual(post["download_type"], "thumbnail")
+ self.assertEqual(post["format"], "jpg")
+ self.assertEqual(post["quality"], "best")
+
+ def test_legacy_captions_with_subtitle_format(self):
+ post = {"format": "captions", "subtitle_format": "vtt", "quality": "best"}
+ main._migrate_legacy_request(post)
+ self.assertEqual(post["download_type"], "captions")
+ self.assertEqual(post["format"], "vtt")
+
+ def test_legacy_video_best_ios(self):
+ post = {"format": "any", "quality": "best_ios", "video_codec": "auto"}
+ main._migrate_legacy_request(post)
+ self.assertEqual(post["download_type"], "video")
+ self.assertEqual(post["format"], "ios")
+ self.assertEqual(post["quality"], "best")
+
+ def test_legacy_video_quality_audio_maps_to_m4a(self):
+ post = {"format": "mp4", "quality": "audio", "video_codec": "h264"}
+ main._migrate_legacy_request(post)
+ self.assertEqual(post["download_type"], "audio")
+ self.assertEqual(post["format"], "m4a")
+ self.assertEqual(post["quality"], "best")
+
+ def test_legacy_video_default(self):
+ post = {"format": "mp4", "quality": "1080", "video_codec": "h265"}
+ main._migrate_legacy_request(post)
+ self.assertEqual(post["download_type"], "video")
+ self.assertEqual(post["codec"], "h265")
+ self.assertEqual(post["format"], "mp4")
+ self.assertEqual(post["quality"], "1080")
+
+
+class ParseLogLevelTests(unittest.TestCase):
+ def test_valid_levels(self):
+ self.assertEqual(main.parseLogLevel("INFO"), logging.INFO)
+ self.assertEqual(main.parseLogLevel("debug"), logging.DEBUG)
+
+ def test_invalid_returns_none(self):
+ self.assertIsNone(main.parseLogLevel("not_a_level"))
+ self.assertIsNone(main.parseLogLevel(123))
+
+
+class ObjectSerializerTests(unittest.TestCase):
+ def test_dict_like_object(self):
+ class Obj:
+ def __init__(self):
+ self.a = 1
+
+ ser = main.ObjectSerializer()
+ self.assertEqual(json.loads(ser.encode(Obj())), {"a": 1})
+
+ def test_generator_becomes_list(self):
+ ser = main.ObjectSerializer()
+
+ def gen():
+ yield 1
+ yield 2
+
+ self.assertEqual(json.loads(ser.encode(gen())), [1, 2])
+
+ def test_string_not_split_to_chars(self):
+ ser = main.ObjectSerializer()
+ self.assertEqual(json.loads(ser.encode("hello")), "hello")
+
+
+class FrontendSafeTests(unittest.TestCase):
+ def test_only_expected_keys(self):
+ safe = main.config.frontend_safe()
+ for key in main.Config._FRONTEND_KEYS:
+ self.assertIn(key, safe)
+ self.assertNotIn("YTDL_OPTIONS", safe)
+ self.assertNotIn("DOWNLOAD_DIR", safe)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/app/tests/test_persistent_queue.py b/app/tests/test_persistent_queue.py
new file mode 100644
index 0000000..1983749
--- /dev/null
+++ b/app/tests/test_persistent_queue.py
@@ -0,0 +1,76 @@
+"""Integration tests for ``PersistentQueue`` (shelve-backed storage)."""
+
+from __future__ import annotations
+
+import os
+import tempfile
+import unittest
+
+from ytdl import DownloadInfo, PersistentQueue
+
+
+class _FakeDownload:
+ __slots__ = ("info",)
+
+ def __init__(self, info: DownloadInfo):
+ self.info = info
+
+
+def _make_info(url: str = "https://example.com/v") -> DownloadInfo:
+ return DownloadInfo(
+ id="id1",
+ title="Title",
+ url=url,
+ quality="best",
+ download_type="video",
+ codec="auto",
+ format="any",
+ folder="",
+ custom_name_prefix="",
+ error=None,
+ entry=None,
+ playlist_item_limit=0,
+ split_by_chapters=False,
+ chapter_template="",
+ )
+
+
+class PersistentQueueTests(unittest.TestCase):
+ def test_put_get_delete_roundtrip(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = os.path.join(tmp, "queue")
+ pq = PersistentQueue("queue", path)
+ dl = _FakeDownload(_make_info("http://a.example"))
+ pq.put(dl)
+ self.assertTrue(pq.exists("http://a.example"))
+ self.assertFalse(pq.empty())
+ got = pq.get("http://a.example")
+ self.assertEqual(got.info.url, "http://a.example")
+ pq.delete("http://a.example")
+ self.assertFalse(pq.exists("http://a.example"))
+
+ def test_saved_items_sorted_by_timestamp(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = os.path.join(tmp, "queue")
+ pq = PersistentQueue("queue", path)
+ a = _FakeDownload(_make_info("http://first.example"))
+ b = _FakeDownload(_make_info("http://second.example"))
+ a.info.timestamp = 100
+ b.info.timestamp = 200
+ pq.put(a)
+ pq.put(b)
+ keys = [k for k, _ in pq.saved_items()]
+ self.assertEqual(keys, ["http://first.example", "http://second.example"])
+
+ def test_load_restores_from_shelve(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = os.path.join(tmp, "queue")
+ pq1 = PersistentQueue("queue", path)
+ pq1.put(_FakeDownload(_make_info("http://load.example")))
+ pq2 = PersistentQueue("queue", path)
+ pq2.load()
+ self.assertTrue(pq2.exists("http://load.example"))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/app/tests/test_ytdl_utils.py b/app/tests/test_ytdl_utils.py
new file mode 100644
index 0000000..9dfd11e
--- /dev/null
+++ b/app/tests/test_ytdl_utils.py
@@ -0,0 +1,146 @@
+"""Tests for pure helpers and migration logic in ``ytdl``."""
+
+from __future__ import annotations
+
+import tempfile
+import unittest
+from pathlib import Path
+
+from ytdl import (
+ DownloadInfo,
+ _convert_generators_to_lists,
+ _convert_srt_to_txt_file,
+ _outtmpl_substitute_field,
+ _sanitize_path_component,
+)
+
+
+class SanitizePathComponentTests(unittest.TestCase):
+ def test_replaces_windows_invalid_chars(self):
+ self.assertEqual(_sanitize_path_component('a:b*c?d"eg|h'), "a_b_c_d_e_f_g_h")
+
+ def test_non_string_passthrough(self):
+ self.assertIs(_sanitize_path_component(None), None)
+ self.assertEqual(_sanitize_path_component(42), 42)
+
+
+class OuttmplSubstituteFieldTests(unittest.TestCase):
+ def test_simple_substitution(self):
+ self.assertEqual(_outtmpl_substitute_field("%(title)s", "title", "Hello"), "Hello")
+
+ def test_format_spec_int(self):
+ self.assertEqual(_outtmpl_substitute_field("%(idx)02d", "idx", 3), "03")
+
+ def test_missing_field_unchanged(self):
+ self.assertEqual(_outtmpl_substitute_field("%(other)s", "title", "x"), "%(other)s")
+
+
+class ConvertGeneratorsToListsTests(unittest.TestCase):
+ def test_nested(self):
+ def g():
+ yield 1
+
+ obj = {"a": g(), "b": [g()]}
+ out = _convert_generators_to_lists(obj)
+ self.assertEqual(out, {"a": [1], "b": [[1]]})
+
+ def test_plain(self):
+ self.assertEqual(_convert_generators_to_lists(5), 5)
+
+
+class ConvertSrtToTxtTests(unittest.TestCase):
+ def test_basic_conversion(self):
+ srt = """1
+00:00:01,000 --> 00:00:02,000
+Hello world
+
+2
+00:00:03,000 --> 00:00:04,000
+Second line
+"""
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / "sub.srt"
+ path.write_text(srt, encoding="utf-8")
+ txt_path = _convert_srt_to_txt_file(str(path))
+ self.assertIsNotNone(txt_path)
+ self.assertTrue(txt_path.endswith(".txt"))
+ content = Path(txt_path).read_text(encoding="utf-8")
+ self.assertIn("Hello world", content)
+ self.assertIn("Second line", content)
+
+
+class DownloadInfoSetstateTests(unittest.TestCase):
+ def _base_state(self, **kwargs):
+ base = {
+ "id": "id1",
+ "title": "t",
+ "url": "http://example.com/v",
+ "folder": "",
+ "custom_name_prefix": "",
+ "error": None,
+ "entry": None,
+ "playlist_item_limit": 0,
+ "split_by_chapters": False,
+ "chapter_template": "",
+ "msg": None,
+ "percent": None,
+ "speed": None,
+ "eta": None,
+ "status": "pending",
+ "size": None,
+ "timestamp": 0,
+ }
+ base.update(kwargs)
+ return base
+
+ def test_migrates_old_audio_format(self):
+ state = self._base_state(format="m4a", quality="best")
+ di = DownloadInfo.__new__(DownloadInfo)
+ di.__setstate__(state)
+ self.assertEqual(di.download_type, "audio")
+ self.assertEqual(di.codec, "auto")
+
+ def test_migrates_thumbnail(self):
+ state = self._base_state(format="thumbnail", quality="best")
+ di = DownloadInfo.__new__(DownloadInfo)
+ di.__setstate__(state)
+ self.assertEqual(di.download_type, "thumbnail")
+ self.assertEqual(di.format, "jpg")
+
+ def test_migrates_captions(self):
+ state = self._base_state(format="captions", subtitle_format="vtt", quality="best")
+ di = DownloadInfo.__new__(DownloadInfo)
+ di.__setstate__(state)
+ self.assertEqual(di.download_type, "captions")
+ self.assertEqual(di.format, "vtt")
+
+ def test_migrates_best_ios(self):
+ state = self._base_state(
+ format="any", quality="best_ios", video_codec="auto"
+ )
+ di = DownloadInfo.__new__(DownloadInfo)
+ di.__setstate__(state)
+ self.assertEqual(di.format, "ios")
+ self.assertEqual(di.quality, "best")
+
+ def test_migrates_quality_audio(self):
+ state = self._base_state(format="mp4", quality="audio", video_codec="h264")
+ di = DownloadInfo.__new__(DownloadInfo)
+ di.__setstate__(state)
+ self.assertEqual(di.download_type, "audio")
+ self.assertEqual(di.format, "m4a")
+
+ def test_new_state_has_subtitle_files(self):
+ state = self._base_state(
+ download_type="video",
+ codec="auto",
+ format="any",
+ quality="best",
+ )
+ di = DownloadInfo.__new__(DownloadInfo)
+ di.__setstate__(state)
+ self.assertEqual(di.subtitle_files, [])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/pyproject.toml b/pyproject.toml
index 54779a4..710988b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,4 +15,13 @@ dependencies = [
[dependency-groups]
dev = [
"pylint",
+ "pytest>=8.0",
+ "pytest-aiohttp>=1.0",
+ "pytest-asyncio>=0.24",
]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+testpaths = ["app/tests"]
+pythonpath = [".", "app"]
+addopts = "-v"
diff --git a/ui/package.json b/ui/package.json
index 37c635d..745ec5b 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -23,14 +23,14 @@
},
"private": true,
"dependencies": {
- "@angular/animations": "^21.2.4",
- "@angular/common": "^21.2.4",
- "@angular/compiler": "^21.2.4",
- "@angular/core": "^21.2.4",
- "@angular/forms": "^21.2.4",
- "@angular/platform-browser": "^21.2.4",
- "@angular/platform-browser-dynamic": "^21.2.4",
- "@angular/service-worker": "^21.2.4",
+ "@angular/animations": "^21.2.5",
+ "@angular/common": "^21.2.5",
+ "@angular/compiler": "^21.2.5",
+ "@angular/core": "^21.2.5",
+ "@angular/forms": "^21.2.5",
+ "@angular/platform-browser": "^21.2.5",
+ "@angular/platform-browser-dynamic": "^21.2.5",
+ "@angular/service-worker": "^21.2.5",
"@fortawesome/angular-fontawesome": "~4.0.0",
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0",
@@ -48,10 +48,10 @@
},
"devDependencies": {
"@angular-eslint/builder": "21.1.0",
- "@angular/build": "^21.2.2",
- "@angular/cli": "^21.2.2",
- "@angular/compiler-cli": "^21.2.4",
- "@angular/localize": "^21.2.4",
+ "@angular/build": "^21.2.3",
+ "@angular/cli": "^21.2.3",
+ "@angular/compiler-cli": "^21.2.5",
+ "@angular/localize": "^21.2.5",
"@eslint/js": "^9.39.4",
"angular-eslint": "21.1.0",
"eslint": "^9.39.4",
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 478432c..dc0e17d 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -9,32 +9,32 @@ importers:
.:
dependencies:
'@angular/animations':
- specifier: ^21.2.4
- version: 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))
'@angular/common':
- specifier: ^21.2.4
- version: 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
'@angular/compiler':
- specifier: ^21.2.4
- version: 21.2.4
+ specifier: ^21.2.5
+ version: 21.2.5
'@angular/core':
- specifier: ^21.2.4
- version: 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
'@angular/forms':
- specifier: ^21.2.4
- version: 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)
'@angular/platform-browser':
- specifier: ^21.2.4
- version: 21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))
'@angular/platform-browser-dynamic':
- specifier: ^21.2.4
- version: 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@21.2.5)(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))
'@angular/service-worker':
- specifier: ^21.2.4
- version: 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
'@fortawesome/angular-fontawesome':
specifier: ~4.0.0
- version: 4.0.0(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))
+ version: 4.0.0(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))
'@fortawesome/fontawesome-svg-core':
specifier: ^7.2.0
version: 7.2.0
@@ -49,10 +49,10 @@ importers:
version: 7.2.0
'@ng-bootstrap/ng-bootstrap':
specifier: ^20.0.0
- version: 20.0.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@popperjs/core@2.11.8)(rxjs@7.8.2)
+ version: 20.0.0(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))(@angular/localize@21.2.5(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5))(@popperjs/core@2.11.8)(rxjs@7.8.2)
'@ng-select/ng-select':
specifier: ^21.5.2
- version: 21.5.2(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))
+ version: 21.5.2(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@@ -61,10 +61,10 @@ importers:
version: 5.3.8(@popperjs/core@2.11.8)
ngx-cookie-service:
specifier: ^21.3.1
- version: 21.3.1(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))
+ version: 21.3.1(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))
ngx-socket-io:
specifier: ~4.10.0
- version: 4.10.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ version: 4.10.0(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
rxjs:
specifier: ~7.8.2
version: 7.8.2
@@ -77,25 +77,25 @@ importers:
devDependencies:
'@angular-eslint/builder':
specifier: 21.1.0
- version: 21.1.0(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)
+ version: 21.1.0(@angular/cli@21.2.3(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)
'@angular/build':
- specifier: ^21.2.2
- version: 21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/service-worker@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@types/node@25.5.0)(chokidar@5.0.0)(postcss@8.5.8)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.0(@types/node@25.5.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3)))
+ specifier: ^21.2.3
+ version: 21.2.3(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5)(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/localize@21.2.5(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/service-worker@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@types/node@25.5.0)(chokidar@5.0.0)(postcss@8.5.8)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.0(@types/node@25.5.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3)))
'@angular/cli':
- specifier: ^21.2.2
- version: 21.2.2(@types/node@25.5.0)(chokidar@5.0.0)
+ specifier: ^21.2.3
+ version: 21.2.3(@types/node@25.5.0)(chokidar@5.0.0)
'@angular/compiler-cli':
- specifier: ^21.2.4
- version: 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3)
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3)
'@angular/localize':
- specifier: ^21.2.4
- version: 21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)
+ specifier: ^21.2.5
+ version: 21.2.5(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5)
'@eslint/js':
specifier: ^9.39.4
version: 9.39.4
angular-eslint:
specifier: 21.1.0
- version: 21.1.0(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript-eslint@8.47.0(eslint@9.39.4)(typescript@5.9.3))(typescript@5.9.3)
+ version: 21.1.0(@angular/cli@21.2.3(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript-eslint@8.47.0(eslint@9.39.4)(typescript@5.9.3))(typescript@5.9.3)
eslint:
specifier: ^9.39.4
version: 9.39.4
@@ -177,13 +177,13 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
- '@angular-devkit/architect@0.2102.2':
- resolution: {integrity: sha512-CDvFtXwyBtMRkTQnm+LfBNLL0yLV8ZGskrM1T6VkcGwXGFDott1FxUdj96ViodYsYL5fbJr0MNA6TlLcanV3kQ==}
+ '@angular-devkit/architect@0.2102.3':
+ resolution: {integrity: sha512-G4wSWUbtWp1WCKw5GMRqHH8g4m5RBpIyzt8n8IX5Pm6iYe/rwCBSKL3ktEkk7AYMwjtonkRlDtAK1GScFsf1Sg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
hasBin: true
- '@angular-devkit/core@21.2.2':
- resolution: {integrity: sha512-xUeKGe4BDQpkz0E6fnAPIJXE0y0nqtap0KhJIBhvN7xi3NenIzTmoi6T9Yv5OOBUdLZbOm4SOel8MhdXiIBpAQ==}
+ '@angular-devkit/core@21.2.3':
+ resolution: {integrity: sha512-i++JVHOijyFckjdYqKbSXUpKnvmO2a0Utt/wQVwiLAT0O9H1hR/2NGPzubB4hnLMNSyVWY8diminaF23mZ0xjA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
peerDependencies:
chokidar: ^5.0.0
@@ -191,8 +191,8 @@ packages:
chokidar:
optional: true
- '@angular-devkit/schematics@21.2.2':
- resolution: {integrity: sha512-CCeyQxGUq+oyGnHd7PfcYIVbj9pRnqjQq0rAojoAqs1BJdtInx9weLBCLy+AjM3NHePeZrnwm+wEVr8apED8kg==}
+ '@angular-devkit/schematics@21.2.3':
+ resolution: {integrity: sha512-tc/bBloRTVIBWGRiMPln1QbW+2QPj+YnWL/nG79abLKWkdrL9dJLcCRXY7dsPNrxOc/QF+8tVpnr8JofhWL9cQ==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
'@angular-eslint/builder@21.1.0':
@@ -239,14 +239,14 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '*'
- '@angular/animations@21.2.4':
- resolution: {integrity: sha512-hO1P7ks9n7lW3D31bzHohSuoAaj05xJUlK8rZgX8IkH5DLx4qhvfNh0t4bbLuBJLP2r1TaLsQ8KFcemCkFRO2w==}
+ '@angular/animations@21.2.5':
+ resolution: {integrity: sha512-8jH48A1gNph5YGlTXXoXJ/5T6uEZB14ITad3uQwBMM1mUUvM0T4QIMk555jIe1fIHHUyTfRR2y7v8SfTe2++fA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
- '@angular/core': 21.2.4
+ '@angular/core': 21.2.5
- '@angular/build@21.2.2':
- resolution: {integrity: sha512-Vq2eIneNxzhHm1MwEmRqEJDwHU9ODfSRDaMWwtysGMhpoMQmLdfTqkQDmkC2qVUr8mV8Z1i5I+oe5ZJaMr/PlQ==}
+ '@angular/build@21.2.3':
+ resolution: {integrity: sha512-u4bhVQruK7KOuHQuoltqlHg+szp0f6rnsGIUolJnT3ez5V6OuSoWIxUorSbvryi2DiKRD/3iwMq7qJN1aN9HCA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
peerDependencies:
'@angular/compiler': ^21.0.0
@@ -256,7 +256,7 @@ packages:
'@angular/platform-browser': ^21.0.0
'@angular/platform-server': ^21.0.0
'@angular/service-worker': ^21.0.0
- '@angular/ssr': ^21.2.2
+ '@angular/ssr': ^21.2.3
karma: ^6.4.0
less: ^4.2.0
ng-packagr: ^21.0.0
@@ -291,38 +291,38 @@ packages:
vitest:
optional: true
- '@angular/cli@21.2.2':
- resolution: {integrity: sha512-eZo8/qX+ZIpIWc0CN+cCX13Lbgi/031wAp8DRVhDDO6SMVtcr/ObOQ2S16+pQdOMXxiG3vby6IhzJuz9WACzMQ==}
+ '@angular/cli@21.2.3':
+ resolution: {integrity: sha512-QzDxnSy8AUOz6ca92xfbNuEmRdWRDi1dfFkxDVr+4l6XUnA9X6VmOi7ioCO1I9oDR73LXHybOqkqHBYDlqt/Ag==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
hasBin: true
- '@angular/common@21.2.4':
- resolution: {integrity: sha512-NrP6qOuUpo3fqq14UJ1b2bIRtWsfvxh1qLqOyFV4gfBrHhXd0XffU1LUlUw1qp4w1uBSgPJ0/N5bSPUWrAguVg==}
+ '@angular/common@21.2.5':
+ resolution: {integrity: sha512-MTjCbsHBkF9W12CW9yYiTJdVfZv/qCqBCZ2iqhMpDA5G+ZJiTKP0IDTJVrx2N5iHfiJ1lnK719t/9GXROtEAvg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
- '@angular/core': 21.2.4
+ '@angular/core': 21.2.5
rxjs: ^6.5.3 || ^7.4.0
- '@angular/compiler-cli@21.2.4':
- resolution: {integrity: sha512-vGjd7DZo/Ox50pQCm5EycmBu91JclimPtZoyNXu/2hSxz3oAkzwiHCwlHwk2g58eheSSp+lYtYRLmHAqSVZLjg==}
+ '@angular/compiler-cli@21.2.5':
+ resolution: {integrity: sha512-Ox3vz6KAM7i47ujR/3M3NCOeCRn6vrC9yV1SHZRhSrYg6CWWcOMveavEEwtNjYtn3hOzrktO4CnuVwtDbU8pLg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
hasBin: true
peerDependencies:
- '@angular/compiler': 21.2.4
+ '@angular/compiler': 21.2.5
typescript: '>=5.9 <6.1'
peerDependenciesMeta:
typescript:
optional: true
- '@angular/compiler@21.2.4':
- resolution: {integrity: sha512-9+ulVK3idIo/Tu4X2ic7/V0+Uj7pqrOAbOuIirYe6Ymm3AjexuFRiGBbfcH0VJhQ5cf8TvIJ1fuh+MI4JiRIxA==}
+ '@angular/compiler@21.2.5':
+ resolution: {integrity: sha512-QloEsknGqLvmr+ED7QShDt7SoMY9mipV+gVnwn4hBI5sbl+TOBfYWXIaJMnxseFwSqjXTSCVGckfylIlynNcFg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
- '@angular/core@21.2.4':
- resolution: {integrity: sha512-2+gd67ZuXHpGOqeb2o7XZPueEWEP81eJza2tSHkT5QMV8lnYllDEmaNnkPxnIjSLGP1O3PmiXxo4z8ibHkLZwg==}
+ '@angular/core@21.2.5':
+ resolution: {integrity: sha512-JgHU134Adb1wrpyGC9ozcv3hiRAgaFTvJFn1u9OU/AVXyxu4meMmVh2hp5QhAvPnv8XQdKWWIkAY+dbpPE6zKA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
- '@angular/compiler': 21.2.4
+ '@angular/compiler': 21.2.5
rxjs: ^6.5.3 || ^7.4.0
zone.js: ~0.15.0 || ~0.16.0
peerDependenciesMeta:
@@ -331,49 +331,49 @@ packages:
zone.js:
optional: true
- '@angular/forms@21.2.4':
- resolution: {integrity: sha512-1fOhctA9ADEBYjI3nPQUR5dHsK2+UWAjup37Ksldk/k0w8UpD5YsN7JVNvsDMZRFMucKYcGykPblU7pABtsqnQ==}
+ '@angular/forms@21.2.5':
+ resolution: {integrity: sha512-pqRuK+a1ZAFZbs8/dZoorFJah2IWaf/SH8axHUpaDJ7fyNrwNEcpczyObdxZ00lOgORpKAhWo/q0hlVS+In8cw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
- '@angular/common': 21.2.4
- '@angular/core': 21.2.4
- '@angular/platform-browser': 21.2.4
+ '@angular/common': 21.2.5
+ '@angular/core': 21.2.5
+ '@angular/platform-browser': 21.2.5
rxjs: ^6.5.3 || ^7.4.0
- '@angular/localize@21.2.4':
- resolution: {integrity: sha512-brKKeH+jaTlY4coIOinKQtitLCguQzyniKYtfrhCvZSN0ap4W4PljAT5w3l+1a8e7/ThM1JVQpqtVCCcJHJZSg==}
+ '@angular/localize@21.2.5':
+ resolution: {integrity: sha512-L/Aa+wMONTM3tvHczwHLYwKwgFhjXwU+TDYJFswu1/nFJ2epb0yNrJzgi9dHXDAMdihJy8920dZr9BI6J/OZ5A==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
hasBin: true
peerDependencies:
- '@angular/compiler': 21.2.4
- '@angular/compiler-cli': 21.2.4
+ '@angular/compiler': 21.2.5
+ '@angular/compiler-cli': 21.2.5
- '@angular/platform-browser-dynamic@21.2.4':
- resolution: {integrity: sha512-LRJLnGh4rdgD0+S5xuDd4YRm5bV8WP2e6F1Pe5rIr6N4V9ofgpB0/uOjYy9se99FJZjoyPnpxaKsp8+XA753Zg==}
+ '@angular/platform-browser-dynamic@21.2.5':
+ resolution: {integrity: sha512-0yDogezPC4OaqkvL/3Pa5mBodOCCUnO4CTOxC+fPy7L+dRhQfVEwtOsN9XkZv5eMGemGeCcNKdchSuYsVkCA2g==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
- '@angular/common': 21.2.4
- '@angular/compiler': 21.2.4
- '@angular/core': 21.2.4
- '@angular/platform-browser': 21.2.4
+ '@angular/common': 21.2.5
+ '@angular/compiler': 21.2.5
+ '@angular/core': 21.2.5
+ '@angular/platform-browser': 21.2.5
- '@angular/platform-browser@21.2.4':
- resolution: {integrity: sha512-1A9e/cQVu+3BkRCktLcO3RZGuw8NOTHw1frUUrpAz+iMyvIT4sDRFbL+U1g8qmOCZqRNC1Pi1HZfZ1kl6kvrcQ==}
+ '@angular/platform-browser@21.2.5':
+ resolution: {integrity: sha512-VuuYguxjgyI4XWuoXrKynmuA3FB991pXbkNhxHeCW0yX+7DGOnGLPF1oierd4/X+IvskmN8foBZLfjyg9u4Ffg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
- '@angular/animations': 21.2.4
- '@angular/common': 21.2.4
- '@angular/core': 21.2.4
+ '@angular/animations': 21.2.5
+ '@angular/common': 21.2.5
+ '@angular/core': 21.2.5
peerDependenciesMeta:
'@angular/animations':
optional: true
- '@angular/service-worker@21.2.4':
- resolution: {integrity: sha512-YcPMb0co2hEDwzOG5S27b6f8rotXEUDx88nQuhHDl/ztuzXaxKklJ21qVDVZ0R433YBCRQJl2D6ZrpJojsnBFw==}
+ '@angular/service-worker@21.2.5':
+ resolution: {integrity: sha512-PbkbDuVmpN135bu/XtskkQ1gPVsiGBI+CX9rVUykqomT3y/okW/qaqsYnmzFZedBpZTGKDOaeFGN5GhJj2O22g==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
hasBin: true
peerDependencies:
- '@angular/core': 21.2.4
+ '@angular/core': 21.2.5
rxjs: ^6.5.3 || ^7.4.0
'@asamuzakjp/css-color@4.1.2':
@@ -439,12 +439,12 @@ packages:
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
- '@babel/helpers@7.28.6':
- resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
+ '@babel/helpers@7.29.2':
+ resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
engines: {node: '>=6.9.0'}
- '@babel/parser@7.29.0':
- resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
+ '@babel/parser@7.29.2':
+ resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -496,11 +496,11 @@ packages:
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
engines: {node: '>=20.19.0'}
- '@emnapi/core@1.9.0':
- resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==}
+ '@emnapi/core@1.9.1':
+ resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
- '@emnapi/runtime@1.9.0':
- resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==}
+ '@emnapi/runtime@1.9.1':
+ resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
'@emnapi/wasi-threads@1.2.0':
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
@@ -733,8 +733,8 @@ packages:
resolution: {integrity: sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==}
engines: {node: '>=6'}
- '@gar/promise-retry@1.0.2':
- resolution: {integrity: sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==}
+ '@gar/promise-retry@1.0.3':
+ resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==}
engines: {node: ^20.17.0 || >=22.9.0}
'@harperfast/extended-iterable@1.0.3':
@@ -1501,28 +1501,28 @@ packages:
cpu: [x64]
os: [win32]
- '@schematics/angular@21.2.2':
- resolution: {integrity: sha512-Ywa6HDtX7TRBQZTVMMnxX3Mk7yVnG8KtSFaXWrkx779+q8tqYdBwNwAqbNd4Zatr1GccKaz9xcptHJta5+DTxw==}
+ '@schematics/angular@21.2.3':
+ resolution: {integrity: sha512-rCEprgpNbJLl9Rm/t92eRYc1eIqD4BAJqB1OO8fzQolyDajCcOBpohjXkuLYSwK9RMyS6f+szNnYGOQawlrPYw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
'@sigstore/bundle@4.0.0':
resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==}
engines: {node: ^20.17.0 || >=22.9.0}
- '@sigstore/core@3.1.0':
- resolution: {integrity: sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==}
+ '@sigstore/core@3.2.0':
+ resolution: {integrity: sha512-kxHrDQ9YgfrWUSXU0cjsQGv8JykOFZQ9ErNKbFPWzk3Hgpwu8x2hHrQ9IdA8yl+j9RTLTC3sAF3Tdq1IQCP4oA==}
engines: {node: ^20.17.0 || >=22.9.0}
'@sigstore/protobuf-specs@0.5.0':
resolution: {integrity: sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==}
engines: {node: ^18.17.0 || >=20.5.0}
- '@sigstore/sign@4.1.0':
- resolution: {integrity: sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==}
+ '@sigstore/sign@4.1.1':
+ resolution: {integrity: sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==}
engines: {node: ^20.17.0 || >=22.9.0}
- '@sigstore/tuf@4.0.1':
- resolution: {integrity: sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==}
+ '@sigstore/tuf@4.0.2':
+ resolution: {integrity: sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==}
engines: {node: ^20.17.0 || >=22.9.0}
'@sigstore/verify@3.1.0':
@@ -1603,8 +1603,8 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/project-service@8.57.0':
- resolution: {integrity: sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==}
+ '@typescript-eslint/project-service@8.57.1':
+ resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
@@ -1613,8 +1613,8 @@ packages:
resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/scope-manager@8.57.0':
- resolution: {integrity: sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==}
+ '@typescript-eslint/scope-manager@8.57.1':
+ resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.47.0':
@@ -1623,8 +1623,8 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/tsconfig-utils@8.57.0':
- resolution: {integrity: sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==}
+ '@typescript-eslint/tsconfig-utils@8.57.1':
+ resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
@@ -1640,8 +1640,8 @@ packages:
resolution: {integrity: sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/types@8.57.0':
- resolution: {integrity: sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==}
+ '@typescript-eslint/types@8.57.1':
+ resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.47.0':
@@ -1650,8 +1650,8 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/typescript-estree@8.57.0':
- resolution: {integrity: sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==}
+ '@typescript-eslint/typescript-estree@8.57.1':
+ resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
@@ -1663,8 +1663,8 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/utils@8.57.0':
- resolution: {integrity: sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==}
+ '@typescript-eslint/utils@8.57.1':
+ resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -1674,8 +1674,8 @@ packages:
resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/visitor-keys@8.57.0':
- resolution: {integrity: sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==}
+ '@typescript-eslint/visitor-keys@8.57.1':
+ resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitejs/plugin-basic-ssl@2.1.4':
@@ -1814,8 +1814,8 @@ packages:
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
engines: {node: ^4.5.0 || >= 5.9}
- baseline-browser-mapping@2.10.8:
- resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==}
+ baseline-browser-mapping@2.10.9:
+ resolution: {integrity: sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -1864,8 +1864,8 @@ packages:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
- cacache@20.0.3:
- resolution: {integrity: sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==}
+ cacache@20.0.4:
+ resolution: {integrity: sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==}
engines: {node: ^20.17.0 || >=22.9.0}
call-bind-apply-helpers@1.0.2:
@@ -1880,8 +1880,8 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
- caniuse-lite@1.0.30001779:
- resolution: {integrity: sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==}
+ caniuse-lite@1.0.30001780:
+ resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
@@ -1965,8 +1965,8 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
- core-js@3.48.0:
- resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==}
+ core-js@3.49.0:
+ resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
cors@2.8.6:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
@@ -2038,8 +2038,8 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
- electron-to-chromium@1.5.313:
- resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==}
+ electron-to-chromium@1.5.321:
+ resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -2249,8 +2249,8 @@ packages:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
- flatted@3.4.1:
- resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==}
+ flatted@3.4.2:
+ resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
@@ -2454,8 +2454,8 @@ packages:
resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==}
engines: {node: '>=10'}
- jose@6.2.1:
- resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==}
+ jose@6.2.2:
+ resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -2549,8 +2549,8 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
- make-fetch-happen@15.0.4:
- resolution: {integrity: sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==}
+ make-fetch-happen@15.0.5:
+ resolution: {integrity: sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==}
engines: {node: ^20.17.0 || >=22.9.0}
math-intrinsics@1.1.0:
@@ -2919,10 +2919,6 @@ packages:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
- retry@0.13.1:
- resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
- engines: {node: '>= 4'}
-
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -3041,8 +3037,8 @@ packages:
resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==}
engines: {node: '>=10.0.0'}
- socket.io-parser@4.2.5:
- resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==}
+ socket.io-parser@4.2.6:
+ resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==}
engines: {node: '>=10.0.0'}
socket.io@4.8.3:
@@ -3130,8 +3126,8 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
- tar@7.5.11:
- resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==}
+ tar@7.5.12:
+ resolution: {integrity: sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==}
engines: {node: '>=18'}
tinybench@2.9.0:
@@ -3149,11 +3145,11 @@ packages:
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
engines: {node: '>=14.0.0'}
- tldts-core@7.0.25:
- resolution: {integrity: sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==}
+ tldts-core@7.0.26:
+ resolution: {integrity: sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==}
- tldts@7.0.25:
- resolution: {integrity: sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==}
+ tldts@7.0.26:
+ resolution: {integrity: sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==}
hasBin: true
to-regex-range@5.0.1:
@@ -3172,8 +3168,8 @@ packages:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'}
- ts-api-utils@2.4.0:
- resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
+ ts-api-utils@2.5.0:
+ resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
@@ -3212,14 +3208,6 @@ packages:
resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==}
engines: {node: '>=20.18.1'}
- unique-filename@5.0.0:
- resolution: {integrity: sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==}
- engines: {node: ^20.17.0 || >=22.9.0}
-
- unique-slug@6.0.0:
- resolution: {integrity: sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==}
- engines: {node: ^20.17.0 || >=22.9.0}
-
unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
@@ -3546,14 +3534,14 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
- '@angular-devkit/architect@0.2102.2(chokidar@5.0.0)':
+ '@angular-devkit/architect@0.2102.3(chokidar@5.0.0)':
dependencies:
- '@angular-devkit/core': 21.2.2(chokidar@5.0.0)
+ '@angular-devkit/core': 21.2.3(chokidar@5.0.0)
rxjs: 7.8.2
transitivePeerDependencies:
- chokidar
- '@angular-devkit/core@21.2.2(chokidar@5.0.0)':
+ '@angular-devkit/core@21.2.3(chokidar@5.0.0)':
dependencies:
ajv: 8.18.0
ajv-formats: 3.0.1(ajv@8.18.0)
@@ -3564,9 +3552,9 @@ snapshots:
optionalDependencies:
chokidar: 5.0.0
- '@angular-devkit/schematics@21.2.2(chokidar@5.0.0)':
+ '@angular-devkit/schematics@21.2.3(chokidar@5.0.0)':
dependencies:
- '@angular-devkit/core': 21.2.2(chokidar@5.0.0)
+ '@angular-devkit/core': 21.2.3(chokidar@5.0.0)
jsonc-parser: 3.3.1
magic-string: 0.30.21
ora: 9.3.0
@@ -3574,11 +3562,11 @@ snapshots:
transitivePeerDependencies:
- chokidar
- '@angular-eslint/builder@21.1.0(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)':
+ '@angular-eslint/builder@21.1.0(@angular/cli@21.2.3(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
- '@angular-devkit/architect': 0.2102.2(chokidar@5.0.0)
- '@angular-devkit/core': 21.2.2(chokidar@5.0.0)
- '@angular/cli': 21.2.2(@types/node@25.5.0)(chokidar@5.0.0)
+ '@angular-devkit/architect': 0.2102.3(chokidar@5.0.0)
+ '@angular-devkit/core': 21.2.3(chokidar@5.0.0)
+ '@angular/cli': 21.2.3(@types/node@25.5.0)(chokidar@5.0.0)
eslint: 9.39.4
typescript: 5.9.3
transitivePeerDependencies:
@@ -3586,34 +3574,34 @@ snapshots:
'@angular-eslint/bundled-angular-compiler@21.1.0': {}
- '@angular-eslint/eslint-plugin-template@21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@typescript-eslint/types@8.57.0)(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
+ '@angular-eslint/eslint-plugin-template@21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@typescript-eslint/types@8.57.1)(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@angular-eslint/bundled-angular-compiler': 21.1.0
'@angular-eslint/template-parser': 21.1.0(eslint@9.39.4)(typescript@5.9.3)
- '@angular-eslint/utils': 21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
- '@typescript-eslint/types': 8.57.0
- '@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
+ '@angular-eslint/utils': 21.1.0(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
+ '@typescript-eslint/types': 8.57.1
+ '@typescript-eslint/utils': 8.57.1(eslint@9.39.4)(typescript@5.9.3)
aria-query: 5.3.2
axobject-query: 4.1.0
eslint: 9.39.4
typescript: 5.9.3
- '@angular-eslint/eslint-plugin@21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
+ '@angular-eslint/eslint-plugin@21.1.0(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@angular-eslint/bundled-angular-compiler': 21.1.0
- '@angular-eslint/utils': 21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
- '@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
+ '@angular-eslint/utils': 21.1.0(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.57.1(eslint@9.39.4)(typescript@5.9.3)
eslint: 9.39.4
- ts-api-utils: 2.4.0(typescript@5.9.3)
+ ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
- '@angular-eslint/schematics@21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(@typescript-eslint/types@8.57.0)(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)':
+ '@angular-eslint/schematics@21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@angular/cli@21.2.3(@types/node@25.5.0)(chokidar@5.0.0))(@typescript-eslint/types@8.57.1)(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
- '@angular-devkit/core': 21.2.2(chokidar@5.0.0)
- '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0)
- '@angular-eslint/eslint-plugin': 21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
- '@angular-eslint/eslint-plugin-template': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@typescript-eslint/types@8.57.0)(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
- '@angular/cli': 21.2.2(@types/node@25.5.0)(chokidar@5.0.0)
+ '@angular-devkit/core': 21.2.3(chokidar@5.0.0)
+ '@angular-devkit/schematics': 21.2.3(chokidar@5.0.0)
+ '@angular-eslint/eslint-plugin': 21.1.0(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
+ '@angular-eslint/eslint-plugin-template': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@typescript-eslint/types@8.57.1)(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
+ '@angular/cli': 21.2.3(@types/node@25.5.0)(chokidar@5.0.0)
ignore: 7.0.5
semver: 7.7.3
strip-json-comments: 3.1.1
@@ -3632,24 +3620,24 @@ snapshots:
eslint-scope: 9.1.2
typescript: 5.9.3
- '@angular-eslint/utils@21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
+ '@angular-eslint/utils@21.1.0(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@angular-eslint/bundled-angular-compiler': 21.1.0
- '@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.57.1(eslint@9.39.4)(typescript@5.9.3)
eslint: 9.39.4
typescript: 5.9.3
- '@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))':
+ '@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))':
dependencies:
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
tslib: 2.8.1
- '@angular/build@21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/service-worker@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@types/node@25.5.0)(chokidar@5.0.0)(postcss@8.5.8)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.0(@types/node@25.5.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3)))':
+ '@angular/build@21.2.3(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5)(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/localize@21.2.5(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/service-worker@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@types/node@25.5.0)(chokidar@5.0.0)(postcss@8.5.8)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.0(@types/node@25.5.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3)))':
dependencies:
'@ampproject/remapping': 2.3.0
- '@angular-devkit/architect': 0.2102.2(chokidar@5.0.0)
- '@angular/compiler': 21.2.4
- '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3)
+ '@angular-devkit/architect': 0.2102.3(chokidar@5.0.0)
+ '@angular/compiler': 21.2.5
+ '@angular/compiler-cli': 21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3)
'@babel/core': 7.29.0
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-split-export-declaration': 7.24.7
@@ -3678,10 +3666,10 @@ snapshots:
vite: 7.3.1(@types/node@25.5.0)(sass@1.97.3)
watchpack: 2.5.1
optionalDependencies:
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
- '@angular/localize': 21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)
- '@angular/platform-browser': 21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))
- '@angular/service-worker': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/localize': 21.2.5(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5)
+ '@angular/platform-browser': 21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))
+ '@angular/service-worker': 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
lmdb: 3.5.1
postcss: 8.5.8
vitest: 4.1.0(@types/node@25.5.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3))
@@ -3698,15 +3686,15 @@ snapshots:
- tsx
- yaml
- '@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0)':
+ '@angular/cli@21.2.3(@types/node@25.5.0)(chokidar@5.0.0)':
dependencies:
- '@angular-devkit/architect': 0.2102.2(chokidar@5.0.0)
- '@angular-devkit/core': 21.2.2(chokidar@5.0.0)
- '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0)
+ '@angular-devkit/architect': 0.2102.3(chokidar@5.0.0)
+ '@angular-devkit/core': 21.2.3(chokidar@5.0.0)
+ '@angular-devkit/schematics': 21.2.3(chokidar@5.0.0)
'@inquirer/prompts': 7.10.1(@types/node@25.5.0)
'@listr2/prompt-adapter-inquirer': 3.0.5(@inquirer/prompts@7.10.1(@types/node@25.5.0))(@types/node@25.5.0)(listr2@9.0.5)
'@modelcontextprotocol/sdk': 1.26.0(zod@4.3.6)
- '@schematics/angular': 21.2.2(chokidar@5.0.0)
+ '@schematics/angular': 21.2.3(chokidar@5.0.0)
'@yarnpkg/lockfile': 1.1.0
algoliasearch: 5.48.1
ini: 6.0.0
@@ -3724,15 +3712,15 @@ snapshots:
- chokidar
- supports-color
- '@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)':
+ '@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)':
dependencies:
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
rxjs: 7.8.2
tslib: 2.8.1
- '@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3)':
+ '@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3)':
dependencies:
- '@angular/compiler': 21.2.4
+ '@angular/compiler': 21.2.5
'@babel/core': 7.29.0
'@jridgewell/sourcemap-codec': 1.5.5
chokidar: 5.0.0
@@ -3746,31 +3734,31 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@angular/compiler@21.2.4':
+ '@angular/compiler@21.2.5':
dependencies:
tslib: 2.8.1
- '@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)':
+ '@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)':
dependencies:
rxjs: 7.8.2
tslib: 2.8.1
optionalDependencies:
- '@angular/compiler': 21.2.4
+ '@angular/compiler': 21.2.5
zone.js: 0.15.0
- '@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)':
+ '@angular/forms@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)':
dependencies:
- '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
- '@angular/platform-browser': 21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))
+ '@angular/common': 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/platform-browser': 21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))
'@standard-schema/spec': 1.1.0
rxjs: 7.8.2
tslib: 2.8.1
- '@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)':
+ '@angular/localize@21.2.5(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5)':
dependencies:
- '@angular/compiler': 21.2.4
- '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3)
+ '@angular/compiler': 21.2.5
+ '@angular/compiler-cli': 21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3)
'@babel/core': 7.29.0
'@types/babel__core': 7.20.5
tinyglobby: 0.2.15
@@ -3778,25 +3766,25 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@angular/platform-browser-dynamic@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))':
+ '@angular/platform-browser-dynamic@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@21.2.5)(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))':
dependencies:
- '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
- '@angular/compiler': 21.2.4
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
- '@angular/platform-browser': 21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))
+ '@angular/common': 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ '@angular/compiler': 21.2.5
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/platform-browser': 21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))
tslib: 2.8.1
- '@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))':
+ '@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))':
dependencies:
- '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/common': 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
tslib: 2.8.1
optionalDependencies:
- '@angular/animations': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))
+ '@angular/animations': 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))
- '@angular/service-worker@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)':
+ '@angular/service-worker@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)':
dependencies:
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
rxjs: 7.8.2
tslib: 2.8.1
@@ -3832,8 +3820,8 @@ snapshots:
'@babel/generator': 7.29.1
'@babel/helper-compilation-targets': 7.28.6
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
- '@babel/helpers': 7.28.6
- '@babel/parser': 7.29.0
+ '@babel/helpers': 7.29.2
+ '@babel/parser': 7.29.2
'@babel/template': 7.28.6
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
@@ -3848,7 +3836,7 @@ snapshots:
'@babel/generator@7.29.1':
dependencies:
- '@babel/parser': 7.29.0
+ '@babel/parser': 7.29.2
'@babel/types': 7.29.0
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
@@ -3894,19 +3882,19 @@ snapshots:
'@babel/helper-validator-option@7.27.1': {}
- '@babel/helpers@7.28.6':
+ '@babel/helpers@7.29.2':
dependencies:
'@babel/template': 7.28.6
'@babel/types': 7.29.0
- '@babel/parser@7.29.0':
+ '@babel/parser@7.29.2':
dependencies:
'@babel/types': 7.29.0
'@babel/template@7.28.6':
dependencies:
'@babel/code-frame': 7.29.0
- '@babel/parser': 7.29.0
+ '@babel/parser': 7.29.2
'@babel/types': 7.29.0
'@babel/traverse@7.29.0':
@@ -3914,7 +3902,7 @@ snapshots:
'@babel/code-frame': 7.29.0
'@babel/generator': 7.29.1
'@babel/helper-globals': 7.28.0
- '@babel/parser': 7.29.0
+ '@babel/parser': 7.29.2
'@babel/template': 7.28.6
'@babel/types': 7.29.0
debug: 4.4.3
@@ -3950,13 +3938,13 @@ snapshots:
'@csstools/css-tokenizer@4.0.0': {}
- '@emnapi/core@1.9.0':
+ '@emnapi/core@1.9.1':
dependencies:
'@emnapi/wasi-threads': 1.2.0
tslib: 2.8.1
optional: true
- '@emnapi/runtime@1.9.0':
+ '@emnapi/runtime@1.9.1':
dependencies:
tslib: 2.8.1
optional: true
@@ -4092,9 +4080,9 @@ snapshots:
'@exodus/bytes@1.15.0': {}
- '@fortawesome/angular-fontawesome@4.0.0(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))':
+ '@fortawesome/angular-fontawesome@4.0.0(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))':
dependencies:
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
'@fortawesome/fontawesome-svg-core': 7.2.0
tslib: 2.8.1
@@ -4116,9 +4104,7 @@ snapshots:
dependencies:
'@fortawesome/fontawesome-common-types': 7.2.0
- '@gar/promise-retry@1.0.2':
- dependencies:
- retry: 0.13.1
+ '@gar/promise-retry@1.0.3': {}
'@harperfast/extended-iterable@1.0.3':
optional: true
@@ -4330,7 +4316,7 @@ snapshots:
express: 5.2.1
express-rate-limit: 8.3.1(express@5.2.1)
hono: 4.12.8
- jose: 6.2.1
+ jose: 6.2.2
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
raw-body: 3.0.2
@@ -4431,26 +4417,26 @@ snapshots:
'@napi-rs/wasm-runtime@1.1.1':
dependencies:
- '@emnapi/core': 1.9.0
- '@emnapi/runtime': 1.9.0
+ '@emnapi/core': 1.9.1
+ '@emnapi/runtime': 1.9.1
'@tybys/wasm-util': 0.10.1
optional: true
- '@ng-bootstrap/ng-bootstrap@20.0.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@popperjs/core@2.11.8)(rxjs@7.8.2)':
+ '@ng-bootstrap/ng-bootstrap@20.0.0(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))(@angular/localize@21.2.5(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5))(@popperjs/core@2.11.8)(rxjs@7.8.2)':
dependencies:
- '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
- '@angular/forms': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)
- '@angular/localize': 21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)
+ '@angular/common': 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/forms': 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)
+ '@angular/localize': 21.2.5(@angular/compiler-cli@21.2.5(@angular/compiler@21.2.5)(typescript@5.9.3))(@angular/compiler@21.2.5)
'@popperjs/core': 2.11.8
rxjs: 7.8.2
tslib: 2.8.1
- '@ng-select/ng-select@21.5.2(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))':
+ '@ng-select/ng-select@21.5.2(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))':
dependencies:
- '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
- '@angular/forms': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)
+ '@angular/common': 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/forms': 21.2.5(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.5(@angular/animations@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)
tslib: 2.8.1
'@nodelib/fs.scandir@2.1.5':
@@ -4481,7 +4467,7 @@ snapshots:
'@npmcli/git@7.0.2':
dependencies:
- '@gar/promise-retry': 1.0.2
+ '@gar/promise-retry': 1.0.3
'@npmcli/promise-spawn': 9.0.1
ini: 6.0.0
lru-cache: 11.2.7
@@ -4706,10 +4692,10 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.59.0':
optional: true
- '@schematics/angular@21.2.2(chokidar@5.0.0)':
+ '@schematics/angular@21.2.3(chokidar@5.0.0)':
dependencies:
- '@angular-devkit/core': 21.2.2(chokidar@5.0.0)
- '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0)
+ '@angular-devkit/core': 21.2.3(chokidar@5.0.0)
+ '@angular-devkit/schematics': 21.2.3(chokidar@5.0.0)
jsonc-parser: 3.3.1
transitivePeerDependencies:
- chokidar
@@ -4718,22 +4704,22 @@ snapshots:
dependencies:
'@sigstore/protobuf-specs': 0.5.0
- '@sigstore/core@3.1.0': {}
+ '@sigstore/core@3.2.0': {}
'@sigstore/protobuf-specs@0.5.0': {}
- '@sigstore/sign@4.1.0':
+ '@sigstore/sign@4.1.1':
dependencies:
+ '@gar/promise-retry': 1.0.3
'@sigstore/bundle': 4.0.0
- '@sigstore/core': 3.1.0
+ '@sigstore/core': 3.2.0
'@sigstore/protobuf-specs': 0.5.0
- make-fetch-happen: 15.0.4
+ make-fetch-happen: 15.0.5
proc-log: 6.1.0
- promise-retry: 2.0.1
transitivePeerDependencies:
- supports-color
- '@sigstore/tuf@4.0.1':
+ '@sigstore/tuf@4.0.2':
dependencies:
'@sigstore/protobuf-specs': 0.5.0
tuf-js: 4.1.0
@@ -4743,7 +4729,7 @@ snapshots:
'@sigstore/verify@3.1.0':
dependencies:
'@sigstore/bundle': 4.0.0
- '@sigstore/core': 3.1.0
+ '@sigstore/core': 3.2.0
'@sigstore/protobuf-specs': 0.5.0
'@socket.io/component-emitter@3.1.2': {}
@@ -4764,7 +4750,7 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
- '@babel/parser': 7.29.0
+ '@babel/parser': 7.29.2
'@babel/types': 7.29.0
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
@@ -4776,7 +4762,7 @@ snapshots:
'@types/babel__template@7.4.4':
dependencies:
- '@babel/parser': 7.29.0
+ '@babel/parser': 7.29.2
'@babel/types': 7.29.0
'@types/babel__traverse@7.28.0':
@@ -4820,7 +4806,7 @@ snapshots:
graphemer: 1.4.0
ignore: 7.0.5
natural-compare: 1.4.0
- ts-api-utils: 2.4.0(typescript@5.9.3)
+ ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -4846,10 +4832,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.57.0(typescript@5.9.3)':
+ '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3)
- '@typescript-eslint/types': 8.57.0
+ '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.57.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
@@ -4860,16 +4846,16 @@ snapshots:
'@typescript-eslint/types': 8.47.0
'@typescript-eslint/visitor-keys': 8.47.0
- '@typescript-eslint/scope-manager@8.57.0':
+ '@typescript-eslint/scope-manager@8.57.1':
dependencies:
- '@typescript-eslint/types': 8.57.0
- '@typescript-eslint/visitor-keys': 8.57.0
+ '@typescript-eslint/types': 8.57.1
+ '@typescript-eslint/visitor-keys': 8.57.1
'@typescript-eslint/tsconfig-utils@8.47.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
- '@typescript-eslint/tsconfig-utils@8.57.0(typescript@5.9.3)':
+ '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
@@ -4880,14 +4866,14 @@ snapshots:
'@typescript-eslint/utils': 8.47.0(eslint@9.39.4)(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.4
- ts-api-utils: 2.4.0(typescript@5.9.3)
+ ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.47.0': {}
- '@typescript-eslint/types@8.57.0': {}
+ '@typescript-eslint/types@8.57.1': {}
'@typescript-eslint/typescript-estree@8.47.0(typescript@5.9.3)':
dependencies:
@@ -4900,22 +4886,22 @@ snapshots:
is-glob: 4.0.3
minimatch: 9.0.9
semver: 7.7.4
- ts-api-utils: 2.4.0(typescript@5.9.3)
+ ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/typescript-estree@8.57.0(typescript@5.9.3)':
+ '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/project-service': 8.57.0(typescript@5.9.3)
- '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3)
- '@typescript-eslint/types': 8.57.0
- '@typescript-eslint/visitor-keys': 8.57.0
+ '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3)
+ '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.57.1
+ '@typescript-eslint/visitor-keys': 8.57.1
debug: 4.4.3
minimatch: 10.2.4
semver: 7.7.4
tinyglobby: 0.2.15
- ts-api-utils: 2.4.0(typescript@5.9.3)
+ ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -4931,12 +4917,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3)':
+ '@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4)
- '@typescript-eslint/scope-manager': 8.57.0
- '@typescript-eslint/types': 8.57.0
- '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.57.1
+ '@typescript-eslint/types': 8.57.1
+ '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
eslint: 9.39.4
typescript: 5.9.3
transitivePeerDependencies:
@@ -4947,9 +4933,9 @@ snapshots:
'@typescript-eslint/types': 8.47.0
eslint-visitor-keys: 4.2.1
- '@typescript-eslint/visitor-keys@8.57.0':
+ '@typescript-eslint/visitor-keys@8.57.1':
dependencies:
- '@typescript-eslint/types': 8.57.0
+ '@typescript-eslint/types': 8.57.1
eslint-visitor-keys: 5.0.1
'@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3))':
@@ -5054,18 +5040,18 @@ snapshots:
'@algolia/requester-fetch': 5.48.1
'@algolia/requester-node-http': 5.48.1
- angular-eslint@21.1.0(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript-eslint@8.47.0(eslint@9.39.4)(typescript@5.9.3))(typescript@5.9.3):
+ angular-eslint@21.1.0(@angular/cli@21.2.3(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript-eslint@8.47.0(eslint@9.39.4)(typescript@5.9.3))(typescript@5.9.3):
dependencies:
- '@angular-devkit/core': 21.2.2(chokidar@5.0.0)
- '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0)
- '@angular-eslint/builder': 21.1.0(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)
- '@angular-eslint/eslint-plugin': 21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
- '@angular-eslint/eslint-plugin-template': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@typescript-eslint/types@8.57.0)(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
- '@angular-eslint/schematics': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(@typescript-eslint/types@8.57.0)(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)
+ '@angular-devkit/core': 21.2.3(chokidar@5.0.0)
+ '@angular-devkit/schematics': 21.2.3(chokidar@5.0.0)
+ '@angular-eslint/builder': 21.1.0(@angular/cli@21.2.3(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)
+ '@angular-eslint/eslint-plugin': 21.1.0(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
+ '@angular-eslint/eslint-plugin-template': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@typescript-eslint/types@8.57.1)(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
+ '@angular-eslint/schematics': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@angular/cli@21.2.3(@types/node@25.5.0)(chokidar@5.0.0))(@typescript-eslint/types@8.57.1)(@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)
'@angular-eslint/template-parser': 21.1.0(eslint@9.39.4)(typescript@5.9.3)
- '@angular/cli': 21.2.2(@types/node@25.5.0)(chokidar@5.0.0)
- '@typescript-eslint/types': 8.57.0
- '@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
+ '@angular/cli': 21.2.3(@types/node@25.5.0)(chokidar@5.0.0)
+ '@typescript-eslint/types': 8.57.1
+ '@typescript-eslint/utils': 8.57.1(eslint@9.39.4)(typescript@5.9.3)
eslint: 9.39.4
typescript: 5.9.3
typescript-eslint: 8.47.0(eslint@9.39.4)(typescript@5.9.3)
@@ -5101,7 +5087,7 @@ snapshots:
base64id@2.0.0: {}
- baseline-browser-mapping@2.10.8: {}
+ baseline-browser-mapping@2.10.9: {}
beasties@0.4.1:
dependencies:
@@ -5158,9 +5144,9 @@ snapshots:
browserslist@4.28.1:
dependencies:
- baseline-browser-mapping: 2.10.8
- caniuse-lite: 1.0.30001779
- electron-to-chromium: 1.5.313
+ baseline-browser-mapping: 2.10.9
+ caniuse-lite: 1.0.30001780
+ electron-to-chromium: 1.5.321
node-releases: 2.0.36
update-browserslist-db: 1.2.3(browserslist@4.28.1)
@@ -5168,7 +5154,7 @@ snapshots:
bytes@3.1.2: {}
- cacache@20.0.3:
+ cacache@20.0.4:
dependencies:
'@npmcli/fs': 5.0.0
fs-minipass: 3.0.3
@@ -5180,7 +5166,6 @@ snapshots:
minipass-pipeline: 1.2.4
p-map: 7.0.4
ssri: 13.0.1
- unique-filename: 5.0.0
call-bind-apply-helpers@1.0.2:
dependencies:
@@ -5194,7 +5179,7 @@ snapshots:
callsites@3.1.0: {}
- caniuse-lite@1.0.30001779: {}
+ caniuse-lite@1.0.30001780: {}
chai@6.2.2: {}
@@ -5258,7 +5243,7 @@ snapshots:
cookie@0.7.2: {}
- core-js@3.48.0: {}
+ core-js@3.49.0: {}
cors@2.8.6:
dependencies:
@@ -5337,7 +5322,7 @@ snapshots:
ee-first@1.1.1: {}
- electron-to-chromium@1.5.313: {}
+ electron-to-chromium@1.5.321: {}
emoji-regex@10.6.0: {}
@@ -5614,10 +5599,10 @@ snapshots:
flat-cache@4.0.1:
dependencies:
- flatted: 3.4.1
+ flatted: 3.4.2
keyv: 4.5.4
- flatted@3.4.1: {}
+ flatted@3.4.2: {}
forwarded@0.2.0: {}
@@ -5791,14 +5776,14 @@ snapshots:
istanbul-lib-instrument@6.0.3:
dependencies:
'@babel/core': 7.29.0
- '@babel/parser': 7.29.0
+ '@babel/parser': 7.29.2
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
semver: 7.7.4
transitivePeerDependencies:
- supports-color
- jose@6.2.1: {}
+ jose@6.2.2: {}
js-tokens@4.0.0: {}
@@ -5919,11 +5904,12 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
- make-fetch-happen@15.0.4:
+ make-fetch-happen@15.0.5:
dependencies:
- '@gar/promise-retry': 1.0.2
+ '@gar/promise-retry': 1.0.3
'@npmcli/agent': 4.0.0
- cacache: 20.0.3
+ '@npmcli/redact': 4.0.0
+ cacache: 20.0.4
http-cache-semantics: 4.2.0
minipass: 7.1.3
minipass-fetch: 5.0.2
@@ -6041,17 +6027,17 @@ snapshots:
negotiator@1.0.0: {}
- ngx-cookie-service@21.3.1(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)):
+ ngx-cookie-service@21.3.1(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)):
dependencies:
- '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
+ '@angular/common': 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
tslib: 2.8.1
- ngx-socket-io@4.10.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2):
+ ngx-socket-io@4.10.0(@angular/common@21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2):
dependencies:
- '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
- '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)
- core-js: 3.48.0
+ '@angular/common': 21.2.5(@angular/core@21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
+ '@angular/core': 21.2.5(@angular/compiler@21.2.5)(rxjs@7.8.2)(zone.js@0.15.0)
+ core-js: 3.49.0
reflect-metadata: 0.2.2
rxjs: 7.8.2
socket.io: 4.8.3
@@ -6078,11 +6064,11 @@ snapshots:
env-paths: 2.2.1
exponential-backoff: 3.1.3
graceful-fs: 4.2.11
- make-fetch-happen: 15.0.4
+ make-fetch-happen: 15.0.5
nopt: 9.0.0
proc-log: 6.1.0
semver: 7.7.4
- tar: 7.5.11
+ tar: 7.5.12
tinyglobby: 0.2.15
which: 6.0.1
transitivePeerDependencies:
@@ -6127,7 +6113,7 @@ snapshots:
dependencies:
'@npmcli/redact': 4.0.0
jsonparse: 1.3.1
- make-fetch-happen: 15.0.4
+ make-fetch-happen: 15.0.5
minipass: 7.1.3
minipass-fetch: 5.0.2
minizlib: 3.1.0
@@ -6198,7 +6184,7 @@ snapshots:
'@npmcli/package-json': 7.0.5
'@npmcli/promise-spawn': 9.0.1
'@npmcli/run-script': 10.0.4
- cacache: 20.0.3
+ cacache: 20.0.4
fs-minipass: 3.0.3
minipass: 7.1.3
npm-package-arg: 13.0.2
@@ -6209,7 +6195,7 @@ snapshots:
promise-retry: 2.0.1
sigstore: 4.1.0
ssri: 13.0.1
- tar: 7.5.11
+ tar: 7.5.12
transitivePeerDependencies:
- supports-color
@@ -6318,8 +6304,6 @@ snapshots:
retry@0.12.0: {}
- retry@0.13.1: {}
-
reusify@1.1.0: {}
rfdc@1.4.1: {}
@@ -6480,10 +6464,10 @@ snapshots:
sigstore@4.1.0:
dependencies:
'@sigstore/bundle': 4.0.0
- '@sigstore/core': 3.1.0
+ '@sigstore/core': 3.2.0
'@sigstore/protobuf-specs': 0.5.0
- '@sigstore/sign': 4.1.0
- '@sigstore/tuf': 4.0.1
+ '@sigstore/sign': 4.1.1
+ '@sigstore/tuf': 4.0.2
'@sigstore/verify': 3.1.0
transitivePeerDependencies:
- supports-color
@@ -6514,13 +6498,13 @@ snapshots:
'@socket.io/component-emitter': 3.1.2
debug: 4.4.3
engine.io-client: 6.6.4
- socket.io-parser: 4.2.5
+ socket.io-parser: 4.2.6
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
- socket.io-parser@4.2.5:
+ socket.io-parser@4.2.6:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.4.3
@@ -6535,7 +6519,7 @@ snapshots:
debug: 4.4.3
engine.io: 6.6.6
socket.io-adapter: 2.5.6
- socket.io-parser: 4.2.5
+ socket.io-parser: 4.2.6
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -6619,7 +6603,7 @@ snapshots:
symbol-tree@3.2.4: {}
- tar@7.5.11:
+ tar@7.5.12:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
@@ -6638,11 +6622,11 @@ snapshots:
tinyrainbow@3.1.0: {}
- tldts-core@7.0.25: {}
+ tldts-core@7.0.26: {}
- tldts@7.0.25:
+ tldts@7.0.26:
dependencies:
- tldts-core: 7.0.25
+ tldts-core: 7.0.26
to-regex-range@5.0.1:
dependencies:
@@ -6652,13 +6636,13 @@ snapshots:
tough-cookie@6.0.1:
dependencies:
- tldts: 7.0.25
+ tldts: 7.0.26
tr46@6.0.0:
dependencies:
punycode: 2.3.1
- ts-api-utils@2.4.0(typescript@5.9.3):
+ ts-api-utils@2.5.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
@@ -6668,7 +6652,7 @@ snapshots:
dependencies:
'@tufjs/models': 4.1.0
debug: 4.4.3
- make-fetch-happen: 15.0.4
+ make-fetch-happen: 15.0.5
transitivePeerDependencies:
- supports-color
@@ -6699,14 +6683,6 @@ snapshots:
undici@7.22.0: {}
- unique-filename@5.0.0:
- dependencies:
- unique-slug: 6.0.0
-
- unique-slug@6.0.0:
- dependencies:
- imurmurhash: 0.1.4
-
unpipe@1.0.0: {}
update-browserslist-db@1.2.3(browserslist@4.28.1):
diff --git a/ui/src/app/app.spec.ts b/ui/src/app/app.spec.ts
index 75934f0..6723156 100644
--- a/ui/src/app/app.spec.ts
+++ b/ui/src/app/app.spec.ts
@@ -1,24 +1,20 @@
import { TestBed } from '@angular/core/testing';
import { App } from './app';
-vi.hoisted(() => {
- Object.defineProperty(window, "matchMedia", {
- writable: true,
- enumerable: true,
- value: vi.fn().mockImplementation((query) => ({
- matches: false,
- media: query,
- onchange: null,
- addEventListener: vi.fn(),
- removeEventListener: vi.fn(),
- dispatchEvent: vi.fn(),
- })),
- });
-});
-
-
describe('App', () => {
beforeEach(async () => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ enumerable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
await TestBed.configureTestingModule({
imports: [App],
}).compileComponents();
@@ -29,5 +25,4 @@ describe('App', () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
-
});
diff --git a/ui/src/app/components/master-checkbox.component.spec.ts b/ui/src/app/components/master-checkbox.component.spec.ts
new file mode 100644
index 0000000..5c5202c
--- /dev/null
+++ b/ui/src/app/components/master-checkbox.component.spec.ts
@@ -0,0 +1,23 @@
+import { TestBed } from '@angular/core/testing';
+import { SelectAllCheckboxComponent } from './master-checkbox.component';
+import { Checkable } from '../interfaces';
+
+describe('SelectAllCheckboxComponent', () => {
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [SelectAllCheckboxComponent],
+ }).compileComponents();
+ });
+
+ it('clicked sets checked on all list items', () => {
+ const fixture = TestBed.createComponent(SelectAllCheckboxComponent);
+ const list = new Map();
+ list.set('u1', { checked: false });
+ fixture.componentRef.setInput('id', 'queue');
+ fixture.componentRef.setInput('list', list);
+ fixture.componentInstance.selected = true;
+ fixture.detectChanges();
+ fixture.componentInstance.clicked();
+ expect(list.get('u1')?.checked).toBe(true);
+ });
+});
diff --git a/ui/src/app/components/slave-checkbox.component.spec.ts b/ui/src/app/components/slave-checkbox.component.spec.ts
new file mode 100644
index 0000000..460b831
--- /dev/null
+++ b/ui/src/app/components/slave-checkbox.component.spec.ts
@@ -0,0 +1,25 @@
+import { TestBed } from '@angular/core/testing';
+import { SelectAllCheckboxComponent } from './master-checkbox.component';
+import { ItemCheckboxComponent } from './slave-checkbox.component';
+
+describe('ItemCheckboxComponent', () => {
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ItemCheckboxComponent, SelectAllCheckboxComponent],
+ }).compileComponents();
+ });
+
+ it('creates with master and checkable inputs', () => {
+ const masterFixture = TestBed.createComponent(SelectAllCheckboxComponent);
+ masterFixture.componentRef.setInput('id', 'q');
+ masterFixture.componentRef.setInput('list', new Map());
+ masterFixture.detectChanges();
+
+ const itemFixture = TestBed.createComponent(ItemCheckboxComponent);
+ itemFixture.componentRef.setInput('id', 'row1');
+ itemFixture.componentRef.setInput('master', masterFixture.componentInstance);
+ itemFixture.componentRef.setInput('checkable', { checked: false });
+ itemFixture.detectChanges();
+ expect(itemFixture.componentInstance).toBeTruthy();
+ });
+});
diff --git a/ui/src/app/pipes/eta.pipe.spec.ts b/ui/src/app/pipes/eta.pipe.spec.ts
new file mode 100644
index 0000000..ec90e8e
--- /dev/null
+++ b/ui/src/app/pipes/eta.pipe.spec.ts
@@ -0,0 +1,26 @@
+import { EtaPipe } from './eta.pipe';
+
+describe('EtaPipe', () => {
+ it('returns null for null input', () => {
+ const pipe = new EtaPipe();
+ expect(pipe.transform(null as unknown as number)).toBeNull();
+ });
+
+ it('formats seconds under one minute', () => {
+ const pipe = new EtaPipe();
+ expect(pipe.transform(0)).toBe('0s');
+ expect(pipe.transform(59)).toBe('59s');
+ });
+
+ it('formats minutes and seconds', () => {
+ const pipe = new EtaPipe();
+ expect(pipe.transform(60)).toBe('1m 0s');
+ expect(pipe.transform(90)).toBe('1m 30s');
+ });
+
+ it('formats hours', () => {
+ const pipe = new EtaPipe();
+ expect(pipe.transform(3600)).toBe('1h 0m 0s');
+ expect(pipe.transform(3661)).toBe('1h 1m 1s');
+ });
+});
diff --git a/ui/src/app/pipes/file-size.pipe.spec.ts b/ui/src/app/pipes/file-size.pipe.spec.ts
new file mode 100644
index 0000000..06b649a
--- /dev/null
+++ b/ui/src/app/pipes/file-size.pipe.spec.ts
@@ -0,0 +1,24 @@
+import { FileSizePipe } from './file-size.pipe';
+
+describe('FileSizePipe', () => {
+ it('returns 0 Bytes for zero or NaN', () => {
+ const pipe = new FileSizePipe();
+ expect(pipe.transform(0)).toBe('0 Bytes');
+ expect(pipe.transform(Number.NaN)).toBe('0 Bytes');
+ });
+
+ it('formats bytes and larger units', () => {
+ const pipe = new FileSizePipe();
+ expect(pipe.transform(500)).toContain('Bytes');
+ expect(pipe.transform(1000)).toContain('KB');
+ expect(pipe.transform(1000 * 1000)).toContain('MB');
+ expect(pipe.transform(1000 ** 3)).toContain('GB');
+ });
+
+ it('handles boundaries between units', () => {
+ const pipe = new FileSizePipe();
+ expect(pipe.transform(999)).toContain('Bytes');
+ expect(pipe.transform(1000)).toContain('KB');
+ expect(pipe.transform(1001)).toContain('KB');
+ });
+});
diff --git a/ui/src/app/pipes/speed.pipe.spec.ts b/ui/src/app/pipes/speed.pipe.spec.ts
index f28f597..4be3db2 100644
--- a/ui/src/app/pipes/speed.pipe.spec.ts
+++ b/ui/src/app/pipes/speed.pipe.spec.ts
@@ -12,4 +12,10 @@ describe('SpeedPipe', () => {
expect(pipe.transform(1024)).toBe('1 KB/s');
expect(pipe.transform(1536)).toBe('1.5 KB/s');
});
+
+ it('formats MB/s and GB/s', () => {
+ const pipe = new SpeedPipe();
+ expect(pipe.transform(1024 * 1024)).toBe('1 MB/s');
+ expect(pipe.transform(1024 * 1024 * 1024)).toBe('1 GB/s');
+ });
});
diff --git a/ui/src/app/services/downloads.service.spec.ts b/ui/src/app/services/downloads.service.spec.ts
new file mode 100644
index 0000000..29e4406
--- /dev/null
+++ b/ui/src/app/services/downloads.service.spec.ts
@@ -0,0 +1,279 @@
+import { TestBed } from '@angular/core/testing';
+import { provideHttpClient, HttpErrorResponse } from '@angular/common/http';
+import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
+import { Subject } from 'rxjs';
+import { DownloadsService, AddDownloadPayload } from './downloads.service';
+import { MeTubeSocket } from './metube-socket.service';
+import { Download } from '../interfaces';
+
+class MeTubeSocketStub {
+ private subjects: Record> = {};
+
+ fromEvent(event: string) {
+ if (!this.subjects[event]) {
+ this.subjects[event] = new Subject();
+ }
+ return this.subjects[event].asObservable();
+ }
+
+ emit(event: string, data: string) {
+ if (!this.subjects[event]) {
+ this.subjects[event] = new Subject();
+ }
+ this.subjects[event].next(data);
+ }
+}
+
+function basePayload(): AddDownloadPayload {
+ return {
+ url: 'https://example.com/v',
+ downloadType: 'video',
+ codec: 'auto',
+ quality: 'best',
+ format: 'any',
+ folder: '',
+ customNamePrefix: '',
+ playlistItemLimit: 0,
+ autoStart: true,
+ splitByChapters: false,
+ chapterTemplate: '',
+ subtitleLanguage: 'en',
+ subtitleMode: 'prefer_manual',
+ };
+}
+
+describe('DownloadsService', () => {
+ let socket: MeTubeSocketStub;
+ let httpMock: HttpTestingController;
+ let service: DownloadsService;
+
+ beforeEach(async () => {
+ socket = new MeTubeSocketStub();
+ await TestBed.configureTestingModule({
+ providers: [
+ DownloadsService,
+ provideHttpClient(),
+ provideHttpClientTesting(),
+ { provide: MeTubeSocket, useValue: socket },
+ ],
+ }).compileComponents();
+
+ service = TestBed.inject(DownloadsService);
+ httpMock = TestBed.inject(HttpTestingController);
+ });
+
+ it('add() posts snake_case fields matching backend', () => {
+ service.add(basePayload()).subscribe();
+ const req = httpMock.expectOne('add');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(
+ expect.objectContaining({
+ url: 'https://example.com/v',
+ download_type: 'video',
+ codec: 'auto',
+ quality: 'best',
+ format: 'any',
+ playlist_item_limit: 0,
+ auto_start: true,
+ split_by_chapters: false,
+ chapter_template: '',
+ subtitle_language: 'en',
+ subtitle_mode: 'prefer_manual',
+ }),
+ );
+ req.flush({ status: 'ok' });
+ });
+
+ it('cancelAdd posts to cancel-add', () => {
+ service.cancelAdd().subscribe();
+ const req = httpMock.expectOne('cancel-add');
+ expect(req.request.method).toBe('POST');
+ req.flush({ status: 'ok' });
+ });
+
+ it('startById posts ids', () => {
+ service.startById(['a', 'b']).subscribe();
+ const req = httpMock.expectOne('start');
+ expect(req.request.body).toEqual({ ids: ['a', 'b'] });
+ req.flush({});
+ });
+
+ it('delById marks items deleting and posts delete', () => {
+ const dl: Download = {
+ id: '1',
+ title: 't',
+ url: 'u1',
+ download_type: 'video',
+ quality: 'best',
+ format: 'any',
+ folder: '',
+ custom_name_prefix: '',
+ playlist_item_limit: 0,
+ status: 'finished',
+ msg: '',
+ percent: 0,
+ speed: 0,
+ eta: 0,
+ filename: '',
+ checked: false,
+ deleting: false,
+ };
+ service.queue.set('u1', dl);
+ service.delById('queue', ['u1']).subscribe();
+ expect(dl.deleting).toBe(true);
+ const req = httpMock.expectOne('delete');
+ expect(req.request.body).toEqual({ where: 'queue', ids: ['u1'] });
+ req.flush({});
+ });
+
+ it('handleHTTPError extracts msg from object body', async () => {
+ const err = new HttpErrorResponse({
+ error: { msg: 'bad' },
+ status: 400,
+ });
+ const res = await new Promise((resolve) => {
+ service.handleHTTPError(err).subscribe(resolve);
+ });
+ expect((res as { status: string }).status).toBe('error');
+ expect((res as { msg?: string }).msg).toBe('bad');
+ });
+
+ it('socket all updates queue and done', () => {
+ const row: Download = {
+ id: '1',
+ title: 't',
+ url: 'u1',
+ download_type: 'video',
+ quality: 'best',
+ format: 'any',
+ folder: '',
+ custom_name_prefix: '',
+ playlist_item_limit: 0,
+ status: 'pending',
+ msg: '',
+ percent: 0,
+ speed: 0,
+ eta: 0,
+ filename: '',
+ checked: false,
+ };
+ const q: [string, Download][] = [['u1', row]];
+ const d: [string, Download][] = [];
+ socket.emit('all', JSON.stringify([q, d]));
+ expect(service.loading).toBe(false);
+ expect(service.queue.has('u1')).toBe(true);
+ });
+
+ it('socket updated preserves checked and deleting', () => {
+ service.queue.set('u1', {
+ id: '1',
+ title: 't',
+ url: 'u1',
+ download_type: 'video',
+ quality: 'best',
+ format: 'any',
+ folder: '',
+ custom_name_prefix: '',
+ playlist_item_limit: 0,
+ status: 'pending',
+ msg: '',
+ percent: 0,
+ speed: 0,
+ eta: 0,
+ filename: '',
+ checked: true,
+ deleting: true,
+ });
+ socket.emit(
+ 'updated',
+ JSON.stringify({ url: 'u1', title: 't', status: 'downloading' }),
+ );
+ const updated = service.queue.get('u1');
+ expect(updated?.checked).toBe(true);
+ expect(updated?.deleting).toBe(true);
+ });
+
+ it('socket completed moves entry to done', () => {
+ service.queue.set('u1', {
+ id: '1',
+ title: 't',
+ url: 'u1',
+ download_type: 'video',
+ quality: 'best',
+ format: 'any',
+ folder: '',
+ custom_name_prefix: '',
+ playlist_item_limit: 0,
+ status: 'pending',
+ msg: '',
+ percent: 0,
+ speed: 0,
+ eta: 0,
+ filename: '',
+ checked: false,
+ });
+ socket.emit('completed', JSON.stringify({ url: 'u1', title: 't', status: 'finished' }));
+ expect(service.queue.has('u1')).toBe(false);
+ expect(service.done.has('u1')).toBe(true);
+ });
+
+ it('socket canceled removes from queue', () => {
+ service.queue.set('u1', {
+ id: '1',
+ title: 't',
+ url: 'u1',
+ download_type: 'video',
+ quality: 'best',
+ format: 'any',
+ folder: '',
+ custom_name_prefix: '',
+ playlist_item_limit: 0,
+ status: 'pending',
+ msg: '',
+ percent: 0,
+ speed: 0,
+ eta: 0,
+ filename: '',
+ checked: false,
+ });
+ socket.emit('canceled', JSON.stringify('u1'));
+ expect(service.queue.has('u1')).toBe(false);
+ });
+
+ it('socket cleared removes from done', () => {
+ service.done.set('u1', {
+ id: '1',
+ title: 't',
+ url: 'u1',
+ download_type: 'video',
+ quality: 'best',
+ format: 'any',
+ folder: '',
+ custom_name_prefix: '',
+ playlist_item_limit: 0,
+ status: 'finished',
+ msg: '',
+ percent: 0,
+ speed: 0,
+ eta: 0,
+ filename: '',
+ checked: false,
+ });
+ socket.emit('cleared', JSON.stringify('u1'));
+ expect(service.done.has('u1')).toBe(false);
+ });
+
+ it('socket configuration updates configuration', () => {
+ socket.emit('configuration', JSON.stringify({ CUSTOM_DIRS: true }));
+ expect(service.configuration['CUSTOM_DIRS']).toBe(true);
+ });
+
+ it('socket custom_dirs updates customDirs', () => {
+ socket.emit('custom_dirs', JSON.stringify({ download_dir: [''] }));
+ expect(service.customDirs['download_dir']).toEqual(['']);
+ });
+
+ afterEach(() => {
+ httpMock.verify();
+ });
+});
diff --git a/uv.lock b/uv.lock
index 01ed742..7dbfc4e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -114,11 +114,11 @@ wheels = [
[[package]]
name = "attrs"
-version = "25.4.0"
+version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
]
[[package]]
@@ -324,15 +324,15 @@ wheels = [
[[package]]
name = "deno"
-version = "2.7.5"
+version = "2.7.7"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/31/8bbaf3fb6a41929ae161be0b2a79b2747b5e5490811573ef60af7e3aeac3/deno-2.7.5.tar.gz", hash = "sha256:50635e0462697fa6e79d90bcacbe98e19f785e604c0e5061754de89b3668af83", size = 8166, upload-time = "2026-03-11T12:48:44.286Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/87/b4/e893908807648b8c499a085cf47c9ca6418a060b0f12e73f128478ada409/deno-2.7.7.tar.gz", hash = "sha256:5798bba73f89ddf50fa33044c8a44fe708fb19ab77b3ef98d02f4124e760fb65", size = 8166, upload-time = "2026-03-19T13:57:09.905Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/68/15/47c4b8da4e1b312ab14a2517e3f484c4d67a879cb5099cb6c33b8ce00c8c/deno-2.7.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29cb89cdaea5f36133841fb4da058b1c6cb70d117ebfc7a24c717747b58e8503", size = 46641593, upload-time = "2026-03-11T12:48:16.589Z" },
- { url = "https://files.pythonhosted.org/packages/1c/3a/c3f8842b7499ff3faeb7508711a82b736d3a4c6e0ffb359191386bcf539d/deno-2.7.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6456980341e97e4eb88e0c560fa57cd1b5f732e0eaadccc6c47d5ada73a71ff3", size = 43537874, upload-time = "2026-03-11T12:48:21.958Z" },
- { url = "https://files.pythonhosted.org/packages/71/a2/53a013ba3509648582748678d5c6980210a45e0913934f91bfe1ec237e07/deno-2.7.5-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:fdc1e647a06ef792643237c030f45295692b0abc05d5bc9894fb11fd70876953", size = 47265090, upload-time = "2026-03-11T12:48:26.819Z" },
- { url = "https://files.pythonhosted.org/packages/3e/85/88c76daa72575f7229bb94191f15f4771f0614227bf8467bfe06e051f4ab/deno-2.7.5-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:c15e6b8ccf5f0808cd5ba243ea4eea7d8d78f6fdff228f5c6c85b96ba286bd3c", size = 49262188, upload-time = "2026-03-11T12:48:32.125Z" },
- { url = "https://files.pythonhosted.org/packages/42/5e/501a92ef93d6d46ed8a1a8c03cff8bcbccbc06c1f59b163113ff09cd23cf/deno-2.7.5-py3-none-win_amd64.whl", hash = "sha256:3e3d06006ee39901dd23068c4a501a4a524fb71c323e22503b1b2ddf236da463", size = 48481169, upload-time = "2026-03-11T12:48:38.684Z" },
+ { url = "https://files.pythonhosted.org/packages/02/08/362f834c64798033ca56a02a1a4e8feca653b9b767aab4a854069ba8c801/deno-2.7.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:70be65294ee575b2e5ee66b587c459500984b1df17505fd6f5e7bffad402de0f", size = 46934365, upload-time = "2026-03-19T13:56:54.324Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/cdbe9daa33e997f26610ee7f554e51ba2c8fd7a18abcbc9c6069e6386164/deno-2.7.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:65641b2dd299e3a4aae4f080d4e32d632bbcf44d77f72f97f61aa7b68ded4747", size = 43831345, upload-time = "2026-03-19T13:56:57.565Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e7/5f63b2a64fc2f7a7ce6c73e9e847c41034283890e6edec0b2791518b7edd/deno-2.7.7-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:cc90d761472df285a8709483d3615fbd2faf4bbc162530196b5a112e4a561016", size = 47571993, upload-time = "2026-03-19T13:57:00.833Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f2/68f4bb53de09970744f905628cff011bd6964f2f00f263140dcc9412a7b5/deno-2.7.7-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ced70363e30a7e3f27f614ffd46d69ccf1dd57633f0df6a3c6375ed2c803aa7", size = 49577613, upload-time = "2026-03-19T13:57:03.766Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/db/2fa6239c0d4df46ef6f3f43d55133aeda6cdd6668c6044d275548a95da24/deno-2.7.7-py3-none-win_amd64.whl", hash = "sha256:e614f666c169ade86a3a089a15a32b9a2002d1ad3294f1fbc8a1bd50c2bac4ab", size = 48802184, upload-time = "2026-03-19T13:57:07.328Z" },
]
[[package]]
@@ -435,6 +435,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
[[package]]
name = "isort"
version = "8.0.1"
@@ -469,6 +478,9 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "pylint" },
+ { name = "pytest" },
+ { name = "pytest-aiohttp" },
+ { name = "pytest-asyncio" },
]
[package.metadata]
@@ -482,7 +494,12 @@ requires-dist = [
]
[package.metadata.requires-dev]
-dev = [{ name = "pylint" }]
+dev = [
+ { name = "pylint" },
+ { name = "pytest", specifier = ">=8.0" },
+ { name = "pytest-aiohttp", specifier = ">=1.0" },
+ { name = "pytest-asyncio", specifier = ">=0.24" },
+]
[[package]]
name = "multidict"
@@ -574,6 +591,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391, upload-time = "2023-09-03T16:33:29.955Z" },
]
+[[package]]
+name = "packaging"
+version = "26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
+]
+
[[package]]
name = "platformdirs"
version = "4.9.4"
@@ -583,6 +609,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
]
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
[[package]]
name = "propcache"
version = "0.4.1"
@@ -691,6 +726,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" },
]
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
[[package]]
name = "pylint"
version = "4.0.5"
@@ -709,6 +753,48 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" },
]
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "pytest-aiohttp"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/4b/d326890c153f2c4ce1bf45d07683c08c10a1766058a22934620bc6ac6592/pytest_aiohttp-1.1.0.tar.gz", hash = "sha256:147de8cb164f3fc9d7196967f109ab3c0b93ea3463ab50631e56438eab7b5adc", size = 12842, upload-time = "2025-01-23T12:44:04.465Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/0f/e6af71c02e0f1098eaf7d2dbf3ffdf0a69fc1e0ef174f96af05cef161f1b/pytest_aiohttp-1.1.0-py3-none-any.whl", hash = "sha256:f39a11693a0dce08dd6c542d241e199dd8047a6e6596b2bcfa60d373f143456d", size = 8932, upload-time = "2025-01-23T12:44:03.27Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
+]
+
[[package]]
name = "python-engineio"
version = "4.13.1"