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"